From 51e1fffa4800fd76baade9c98bc15f00a78ff2de Mon Sep 17 00:00:00 2001 From: Austin Golding Date: Tue, 22 Aug 2023 18:07:55 -0700 Subject: [PATCH] Add ability to view cobalt strike raw log files (#176) --- .../client/src/store/campaign/campaign.ts | 11 ++++ .../src/store/graphql/RootStore.base.ts | 15 +++++ .../client/src/store/util/cleaned-queries.ts | 1 + .../Explore/Panels/Command/RawLogsDialog.tsx | 66 ++++++++++++++----- applications/server/schema.graphql | 3 + applications/server/src/config.ts | 4 +- .../server/src/store/command-resolvers.ts | 2 +- .../server/src/store/log-resolvers.ts | 23 +++++++ .../server/src/store/operator-resolvers.ts | 4 +- 9 files changed, 110 insertions(+), 19 deletions(-) diff --git a/applications/client/src/store/campaign/campaign.ts b/applications/client/src/store/campaign/campaign.ts index dd994324..77cb5e14 100644 --- a/applications/client/src/store/campaign/campaign.ts +++ b/applications/client/src/store/campaign/campaign.ts @@ -244,4 +244,15 @@ export class CampaignStore extends ExtendedModel(() => ({ direction: direction as SortDirection, }; } + + @computed get parsers(): string[] { + if (this.id) + return ( + (this.appStore?.graphqlStore.campaigns + .get(this.id) + ?.parsers?.map((parser) => parser.parserName) + .filter(Boolean) as string[]) || [] + ); + return []; + } } diff --git a/applications/client/src/store/graphql/RootStore.base.ts b/applications/client/src/store/graphql/RootStore.base.ts index 88b4f4a8..e5e09cb8 100644 --- a/applications/client/src/store/graphql/RootStore.base.ts +++ b/applications/client/src/store/graphql/RootStore.base.ts @@ -176,6 +176,7 @@ export enum RootStoreBaseQueries { queryHosts = 'queryHosts', queryImages = 'queryImages', queryLinks = 'queryLinks', + queryLogFilesByBeaconId = 'queryLogFilesByBeaconId', queryLogs = 'queryLogs', queryLogsByBeaconId = 'queryLogsByBeaconId', queryNonHidableEntities = 'queryNonHidableEntities', @@ -633,6 +634,20 @@ export class RootStoreBase extends ExtendedModel( !!clean ); } + // Get logs from beacon sorted by time. The goal is to be able to re-create the full log for that beacon. + @modelAction queryLogFilesByBeaconId( + variables: { beaconId: string; campaignId: string }, + _?: any, + options: QueryOptions = {}, + clean?: boolean + ) { + return this.query<{ logFilesByBeaconId: StringModel[] }>( + `query logFilesByBeaconId($beaconId: String!, $campaignId: String!) { logFilesByBeaconId(beaconId: $beaconId, campaignId: $campaignId) }`, + variables, + options, + !!clean + ); + } // Get log entries by ids @modelAction queryLogs( variables: { beaconId?: string; campaignId: string; hostId?: string }, diff --git a/applications/client/src/store/util/cleaned-queries.ts b/applications/client/src/store/util/cleaned-queries.ts index 0309e40e..77e13157 100644 --- a/applications/client/src/store/util/cleaned-queries.ts +++ b/applications/client/src/store/util/cleaned-queries.ts @@ -70,6 +70,7 @@ export const hostQuery = hostModelPrimitives.cobaltStrikeServer.beaconIds export const campaignsQuery = campaignModelPrimitives .lastOpenedBy((user) => user) .creator((user) => user) + .parsers((parser) => parser.parserName) .toString(); export const rawLogOutputQuery = logEntryModelPrimitives; diff --git a/applications/client/src/views/Campaign/Explore/Panels/Command/RawLogsDialog.tsx b/applications/client/src/views/Campaign/Explore/Panels/Command/RawLogsDialog.tsx index 8a461f4f..ca73a72c 100644 --- a/applications/client/src/views/Campaign/Explore/Panels/Command/RawLogsDialog.tsx +++ b/applications/client/src/views/Campaign/Explore/Panels/Command/RawLogsDialog.tsx @@ -1,6 +1,5 @@ -import type {} from '@blueprintjs/core'; import { Button, ButtonGroup, Divider, NonIdealState, Spinner } from '@blueprintjs/core'; -import { Copy16, UpToTop16 } from '@carbon/icons-react'; +import { Copy16, UpToTop16, Document16 } from '@carbon/icons-react'; import { css } from '@emotion/react'; import type { DialogExProps } from '@redeye/client/components'; import { CarbonIcon, DialogEx } from '@redeye/client/components'; @@ -26,6 +25,7 @@ export const RawLogsDialog = observer(({ ...props }) => { const scrollTopTargetRef = useRef(null); const state = createState({ + showLogFile: false, get isOpen(): boolean { return ( !!(this.beacon && store.router.queryParams['raw-logs']) || @@ -46,6 +46,7 @@ export const RawLogsDialog = observer(({ ...props }) => { return this.command?.input?.current?.id; }, onClose() { + this.showLogFile = false; store.router.updateQueryParams({ queryParams: { 'raw-logs': undefined, 'raw-command': undefined } }); }, handleCopyText(logsByBeaconId) { @@ -79,6 +80,23 @@ export const RawLogsDialog = observer(({ ...props }) => { { enabled: state.isOpen } ); + const { + data: { logFilesByBeaconId } = {}, + isLoading: isLoadingFiles, + isError: isErrorFiles, + } = useQuery( + ['rawLogFile', store.campaign.id, state.commandInputId], + async () => + await store.graphqlStore.queryLogFilesByBeaconId( + { + campaignId: store.campaign.id!, + beaconId: state.beacon?.id!, + }, + selectFromLogEntry().toString() + ), + { enabled: state.isOpen && state.showLogFile && store.campaign.parsers.includes('cobalt-strike-parser') } + ); + if (!state.command && !state.beacon) return null; return ( @@ -106,6 +124,16 @@ export const RawLogsDialog = observer(({ ...props }) => { + {store.campaign.parsers.includes('cobalt-strike-parser') ? ( +