From 20dd329f16bf52afc956d0b7fab7525a050bdb6f Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 30 Mar 2021 22:14:23 -0500 Subject: [PATCH 01/30] Move alert-specific mocks to more declarative mock file --- .../public/common/mock/index.ts | 1 + .../common/mock/mock_detection_alerts.ts | 74 +++++++++++++++++++ .../public/common/mock/mock_ecs.ts | 66 ----------------- 3 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index 469c3d9101eb41..ee34cc1798b548 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -10,6 +10,7 @@ export * from './header'; export * from './hook_wrapper'; export * from './index_pattern'; export * from './mock_detail_item'; +export * from './mock_detection_alerts'; export * from './mock_ecs'; export * from './mock_local_storage'; export * from './mock_timeline_data'; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts new file mode 100644 index 00000000000000..ea44cf0e398229 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Ecs } from '../../../common/ecs'; + +export const mockEcsDataWithAlert: Ecs = { + _id: '1', + timestamp: '2018-11-05T19:03:25.937Z', + host: { + name: ['apache'], + ip: ['192.168.0.1'], + }, + event: { + id: ['1'], + action: ['Action'], + category: ['Access'], + module: ['nginx'], + severity: [3], + }, + source: { + ip: ['192.168.0.1'], + port: [80], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + user: { + id: ['1'], + name: ['john.dee'], + }, + geo: { + region_name: ['xx'], + country_iso_code: ['xx'], + }, + signal: { + rule: { + created_at: ['2020-01-10T21:11:45.839Z'], + updated_at: ['2020-01-10T21:11:45.839Z'], + created_by: ['elastic'], + description: ['24/7'], + enabled: [true], + false_positives: ['test-1'], + filters: [], + from: ['now-300s'], + id: ['b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + immutable: [false], + index: ['auditbeat-*'], + interval: ['5m'], + rule_id: ['rule-id-1'], + language: ['kuery'], + output_index: ['.siem-signals-default'], + max_signals: [100], + risk_score: ['21'], + query: ['user.name: root or user.name: admin'], + references: ['www.test.co'], + saved_id: ["Garrett's IP"], + timeline_id: ['1234-2136-11ea-9864-ebc8cc1cb8c2'], + timeline_title: ['Untitled timeline'], + severity: ['low'], + updated_by: ['elastic'], + tags: [], + to: ['now'], + type: ['saved_query'], + threat: [], + note: ['# this is some markdown documentation'], + version: ['1'], + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_ecs.ts b/x-pack/plugins/security_solution/public/common/mock/mock_ecs.ts index a28c2cc3bc5811..f44c5c335cd218 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_ecs.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_ecs.ts @@ -1026,69 +1026,3 @@ export const mockEcsData: Ecs[] = [ }, }, ]; - -export const mockEcsDataWithAlert: Ecs = { - _id: '1', - timestamp: '2018-11-05T19:03:25.937Z', - host: { - name: ['apache'], - ip: ['192.168.0.1'], - }, - event: { - id: ['1'], - action: ['Action'], - category: ['Access'], - module: ['nginx'], - severity: [3], - }, - source: { - ip: ['192.168.0.1'], - port: [80], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - user: { - id: ['1'], - name: ['john.dee'], - }, - geo: { - region_name: ['xx'], - country_iso_code: ['xx'], - }, - signal: { - rule: { - created_at: ['2020-01-10T21:11:45.839Z'], - updated_at: ['2020-01-10T21:11:45.839Z'], - created_by: ['elastic'], - description: ['24/7'], - enabled: [true], - false_positives: ['test-1'], - filters: [], - from: ['now-300s'], - id: ['b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], - immutable: [false], - index: ['auditbeat-*'], - interval: ['5m'], - rule_id: ['rule-id-1'], - language: ['kuery'], - output_index: ['.siem-signals-default'], - max_signals: [100], - risk_score: ['21'], - query: ['user.name: root or user.name: admin'], - references: ['www.test.co'], - saved_id: ["Garrett's IP"], - timeline_id: ['1234-2136-11ea-9864-ebc8cc1cb8c2'], - timeline_title: ['Untitled timeline'], - severity: ['low'], - updated_by: ['elastic'], - tags: [], - to: ['now'], - type: ['saved_query'], - threat: [], - note: ['# this is some markdown documentation'], - version: ['1'], - }, - }, -}; From 6a1a6133e99a269cb6819c782d511c8442143a92 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 30 Mar 2021 22:24:32 -0500 Subject: [PATCH 02/30] Add placeholder interface for ECS threat fields --- x-pack/plugins/security_solution/common/ecs/index.ts | 2 ++ .../plugins/security_solution/common/ecs/threat/index.ts | 9 +++++++++ .../public/common/mock/mock_detection_alerts.ts | 5 +++++ 3 files changed, 16 insertions(+) create mode 100644 x-pack/plugins/security_solution/common/ecs/threat/index.ts diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index 4c57f6419d5dbf..8054b3c8521db5 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -28,6 +28,7 @@ import { UserEcs } from './user'; import { WinlogEcs } from './winlog'; import { ProcessEcs } from './process'; import { SystemEcs } from './system'; +import { ThreatEcs } from './threat'; import { Ransomware } from './ransomware'; export interface Ecs { @@ -58,6 +59,7 @@ export interface Ecs { process?: ProcessEcs; file?: FileEcs; system?: SystemEcs; + threat?: ThreatEcs; // This should be temporary eql?: { parentId: string; sequenceNumber: string }; Ransomware?: Ransomware; diff --git a/x-pack/plugins/security_solution/common/ecs/threat/index.ts b/x-pack/plugins/security_solution/common/ecs/threat/index.ts new file mode 100644 index 00000000000000..697ce37d9eb9fb --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/threat/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ThreatEcs {} diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts index ea44cf0e398229..3607df2a690100 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts @@ -72,3 +72,8 @@ export const mockEcsDataWithAlert: Ecs = { }, }, }; + +export const getMockDetectionAlert = (overrides: Partial = {}): Ecs => ({ + ...mockEcsDataWithAlert, + ...overrides, +}); From 9b12c87db53ad60f5979256db869fa8ad7408287 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 31 Mar 2021 03:47:20 -0500 Subject: [PATCH 03/30] Test and implement CTI row renderer The display details are not yet implemented, but those will be fleshed out in the ThreatMatchRow component. --- .../common/types/timeline/index.ts | 1 + .../common/mock/mock_detection_alerts.ts | 2 +- .../threat_match_row_renderer.test.tsx.snap | 11 +++ .../renderers/cti/threat_match_row.test.tsx | 45 ++++++++++ .../body/renderers/cti/threat_match_row.tsx | 25 ++++++ .../cti/threat_match_row_renderer.test.tsx | 82 +++++++++++++++++++ .../cti/threat_match_row_renderer.tsx | 26 ++++++ 7 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 5fb7d1a74fc367..9def70048410a9 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -206,6 +206,7 @@ export enum RowRendererId { system_fim = 'system_fim', system_security_event = 'system_security_event', system_socket = 'system_socket', + threat_match = 'threat_match', zeek = 'zeek', } diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts index 3607df2a690100..6d7f48cb9626e9 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts @@ -73,7 +73,7 @@ export const mockEcsDataWithAlert: Ecs = { }, }; -export const getMockDetectionAlert = (overrides: Partial = {}): Ecs => ({ +export const getDetectionAlertMock = (overrides: Partial = {}): Ecs => ({ ...mockEcsDataWithAlert, ...overrides, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap new file mode 100644 index 00000000000000..4ced3b51c7a7f4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1`] = ` + + + + + +`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx new file mode 100644 index 00000000000000..2cb6ef0ae9769a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; +import { getDetectionAlertMock } from '../../../../../../common/mock'; +import { ThreatMatchRow } from './threat_match_row'; + +describe('threatMatchRow', () => { + let alertMock: ReturnType; + let mockTheme: ReturnType; + + beforeEach(() => { + mockTheme = getMockTheme({ eui: { paddingSizes: {} } }); + alertMock = getDetectionAlertMock({ + threat: { + indicator: [ + { + matched: { + type: 'url', + }, + }, + ], + }, + }); + }); + + it('renders an indicator match alert', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="threat-match-row-renderer"]').exists()).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx new file mode 100644 index 00000000000000..7801fc1ed5b54c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Ecs } from '../../../../../../../common/ecs'; +import { BrowserFields } from '../../../../../../common/containers/source'; +import { RowRendererContainer } from '../row_renderer'; + +export const ThreatMatchRow = ({ + browserFields, + data, + timelineId, +}: { + browserFields: BrowserFields; + data: Ecs; + timelineId: string; +}) => ( + + + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx new file mode 100644 index 00000000000000..24078b58b82d53 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { getDetectionAlertMock } from '../../../../../../common/mock'; + +import { threatMatchRowRenderer } from './threat_match_row_renderer'; + +describe('threatMatchRowRenderer', () => { + let alertMock: ReturnType; + + beforeEach(() => { + alertMock = getDetectionAlertMock({ + threat: { + indicator: [ + { + matched: { + type: 'url', + }, + }, + ], + }, + }); + }); + + describe('#isInstance', () => { + it('is false for a minimal event', () => { + const minimalEvent = { + _id: 'abcd', + timestamp: '2018-11-12T19:03:25.936Z', + }; + expect(threatMatchRowRenderer.isInstance(minimalEvent)).toBe(false); + }); + + it('is false for an alert with indicator data but no match', () => { + const indicatorEvent = getDetectionAlertMock({ + threat: { + indicator: { + type: 'url', + }, + }, + }); + expect(threatMatchRowRenderer.isInstance(indicatorEvent)).toBe(false); + }); + + it('is true for any event with indicator match fields', () => { + const indicatorMatchEvent = { + _id: 'abc', + threat: { + indicator: { + matched: { + type: 'ip', + }, + }, + }, + }; + expect(threatMatchRowRenderer.isInstance(indicatorMatchEvent)).toBe(true); + }); + + it('is true for an alert enriched by an indicator match', () => { + expect(threatMatchRowRenderer.isInstance(alertMock)).toBe(true); + }); + }); + + describe('#renderRow', () => { + it('renders correctly against snapshot', () => { + const children = threatMatchRowRenderer.renderRow({ + browserFields: {}, + data: alertMock, + timelineId: 'test', + }); + const wrapper = shallow({children}); + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx new file mode 100644 index 00000000000000..91c41b49808571 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash/fp'; + +import { Ecs } from '../../../../../../../common/ecs'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; +import { asArrayIfExists } from '../../../../../../common/lib/helpers'; +import { RowRenderer } from '../row_renderer'; +import { ThreatMatchRow } from './threat_match_row'; + +const THREAT_INDICATOR_FIELD = 'threat.indicator'; +const isThreatMatch = (ecs: Ecs): boolean => { + const [threatIndicator] = asArrayIfExists(get(THREAT_INDICATOR_FIELD, ecs)) ?? []; + return !!threatIndicator?.matched; +}; + +export const threatMatchRowRenderer: RowRenderer = { + id: RowRendererId.threat_match, + isInstance: isThreatMatch, + renderRow: ThreatMatchRow, +}; From e373e787ef491f61235660f2db71204228b2e233 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 31 Mar 2021 19:58:55 -0500 Subject: [PATCH 04/30] Pass full fields data to our row renderers This data is not used by any existing row renderers and so this commit is mostly just plumbing that data through. This is necessary, however, for our new threat match row renderer as it requires nested fields, which cannot be retrieved through the mechanism that retrieves the existing row renderer data. However, these nested fields are available, if requested, through this other data structure, hence this plumbing. For now to minimize changes I'm marking this as an optional field; however in reality a value will always be present. --- .../components/alerts_table/index.tsx | 6 +- .../timeline/body/events/stateful_event.tsx | 8 +-- .../events/stateful_row_renderer/index.tsx | 5 +- .../cti/threat_match_row_renderer.tsx | 9 +++ .../body/renderers/get_row_renderer.test.tsx | 59 +++++++++---------- .../body/renderers/get_row_renderer.ts | 9 ++- .../timeline/body/renderers/row_renderer.tsx | 5 +- 7 files changed, 62 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 2890eb912b84c2..8279904704ac3d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -47,6 +47,7 @@ import { } from '../../../common/components/toasters'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { requiredFields as requiredThreatMatchFields } from '../../../timelines/components/timeline/body/renderers/cti/threat_match_row_renderer'; import { buildTimeRangeFilter } from './helpers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; @@ -318,7 +319,10 @@ export const AlertsTableComponent: React.FC = ({ id: timelineId, loadingText: i18n.LOADING_ALERTS, selectAll: false, - queryFields: requiredFieldsForActions, + // TODO in the future, our alerts timeline fields should be derived from the + // fields required by enabled row renderers and other functionality; for now we unconditionally + // add the superset of fields. + queryFields: [...requiredFieldsForActions, ...requiredThreatMatchFields], title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 97ab088b615833..019bf2ea3dd72e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -148,10 +148,10 @@ const StatefulEventComponent: React.FC = ({ [notesById, noteIds] ); - const hasRowRenderers: boolean = useMemo(() => getRowRenderer(event.ecs, rowRenderers) != null, [ - event.ecs, - rowRenderers, - ]); + const hasRowRenderers: boolean = useMemo( + () => getRowRenderer(rowRenderers, event.ecs, event.data) != null, + [event.ecs, event.data, rowRenderers] + ); const onToggleShowNotes = useCallback(() => { const eventId = event._id; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx index 10a25538c1ba39..50aa638d1df1c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx @@ -61,8 +61,9 @@ export const StatefulRowRenderer = ({ rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, }); - const rowRenderer = useMemo(() => getRowRenderer(event.ecs, rowRenderers), [ + const rowRenderer = useMemo(() => getRowRenderer(rowRenderers, event.ecs, event.data), [ event.ecs, + event.data, rowRenderers, ]); @@ -80,6 +81,7 @@ export const StatefulRowRenderer = ({ {rowRenderer.renderRow({ browserFields, data: event.ecs, + flattenedData: event.data, timelineId, })} @@ -91,6 +93,7 @@ export const StatefulRowRenderer = ({ ariaRowindex, browserFields, event.ecs, + event.data, focusOwnership, onFocus, onKeyDown, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx index 91c41b49808571..1ca9a8a2de2c8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx @@ -19,6 +19,15 @@ const isThreatMatch = (ecs: Ecs): boolean => { return !!threatIndicator?.matched; }; +export const requiredFields = [ + 'threat.indicator.event.dataset', + 'threat.indicator.event.reference', + 'threat.indicator.provider', + 'threat.indicator.matched.atomic', + 'threat.indicator.matched.field', + 'threat.indicator.matched.type', +]; + export const threatMatchRowRenderer: RowRenderer = { id: RowRendererId.threat_match, isInstance: isThreatMatch, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index b92a4381d837b3..c3c3c5e6b74128 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -6,14 +6,13 @@ */ import { shallow } from 'enzyme'; -import { cloneDeep } from 'lodash'; import React from 'react'; import { removeExternalLinkText } from '../../../../../../common/test_utils'; import '../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; -import { Ecs } from '../../../../../../common/ecs'; import { mockTimelineData } from '../../../../../common/mock'; +import { TimelineItem } from '../../../../../../common/search_strategy'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; @@ -32,26 +31,26 @@ jest.mock('@elastic/eui', () => { jest.mock('../../../../../common/components/link_to'); describe('get_column_renderer', () => { - let nonSuricata: Ecs; - let suricata: Ecs; - let zeek: Ecs; - let system: Ecs; - let auditd: Ecs; + let nonSuricata: TimelineItem; + let suricata: TimelineItem; + let zeek: TimelineItem; + let system: TimelineItem; + let auditd: TimelineItem; const mount = useMountAppended(); beforeEach(() => { - nonSuricata = cloneDeep(mockTimelineData[0].ecs); - suricata = cloneDeep(mockTimelineData[2].ecs); - zeek = cloneDeep(mockTimelineData[13].ecs); - system = cloneDeep(mockTimelineData[28].ecs); - auditd = cloneDeep(mockTimelineData[19].ecs); + nonSuricata = mockTimelineData[0]; + suricata = mockTimelineData[2]; + zeek = mockTimelineData[13]; + system = mockTimelineData[28]; + auditd = mockTimelineData[19]; }); test('renders correctly against snapshot', () => { - const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); + const rowRenderer = getRowRenderer(defaultRowRenderers, nonSuricata.ecs, nonSuricata.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: nonSuricata, + data: nonSuricata.ecs, timelineId: 'test', }); @@ -60,10 +59,10 @@ describe('get_column_renderer', () => { }); test('should render plain row data when it is a non suricata row', () => { - const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); + const rowRenderer = getRowRenderer(defaultRowRenderers, nonSuricata.ecs, nonSuricata.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: nonSuricata, + data: nonSuricata.ecs, timelineId: 'test', }); const wrapper = mount( @@ -75,10 +74,10 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data when it is a suricata row', () => { - const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); + const rowRenderer = getRowRenderer(defaultRowRenderers, suricata.ecs, suricata.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: suricata, + data: suricata.ecs, timelineId: 'test', }); const wrapper = mount( @@ -92,11 +91,11 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data if event.category is network_traffic', () => { - suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); + suricata.ecs.event = { ...suricata.ecs.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(defaultRowRenderers, suricata.ecs, suricata.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: suricata, + data: suricata.ecs, timelineId: 'test', }); const wrapper = mount( @@ -110,11 +109,11 @@ describe('get_column_renderer', () => { }); test('should render a zeek row data if event.category is network_traffic', () => { - zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(zeek, defaultRowRenderers); + zeek.ecs.event = { ...zeek.ecs.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(defaultRowRenderers, zeek.ecs, zeek.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: zeek, + data: zeek.ecs, timelineId: 'test', }); const wrapper = mount( @@ -128,11 +127,11 @@ describe('get_column_renderer', () => { }); test('should render a system row data if event.category is network_traffic', () => { - system.event = { ...system.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(system, defaultRowRenderers); + system.ecs.event = { ...system.ecs.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(defaultRowRenderers, system.ecs, system.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: system, + data: system.ecs, timelineId: 'test', }); const wrapper = mount( @@ -146,11 +145,11 @@ describe('get_column_renderer', () => { }); test('should render a auditd row data if event.category is network_traffic', () => { - auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(auditd, defaultRowRenderers); + auditd.ecs.event = { ...auditd.ecs.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(defaultRowRenderers, auditd.ecs, auditd.data); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: auditd, + data: auditd.ecs, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts index bfe60a14e042de..875721af4b0821 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts @@ -6,7 +6,12 @@ */ import { Ecs } from '../../../../../../common/ecs'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy'; import { RowRenderer } from './row_renderer'; -export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null => - rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null; +export const getRowRenderer = ( + rowRenderers: RowRenderer[], + ecs: Ecs, + data: TimelineNonEcsData[] +): RowRenderer | null => + rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs, data)) ?? null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx index 679da28e622bfb..ed4b5be2b2adca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx @@ -11,6 +11,7 @@ import { BrowserFields } from '../../../../../common/containers/source'; import type { RowRendererId } from '../../../../../../common/types/timeline'; import { Ecs } from '../../../../../../common/ecs'; import { EventsTrSupplement } from '../../styles'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy'; interface RowRendererContainerProps { children: React.ReactNode; @@ -25,14 +26,16 @@ RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { id: RowRendererId; - isInstance: (data: Ecs) => boolean; + isInstance: (data: Ecs, flattenedData?: TimelineNonEcsData[]) => boolean; renderRow: ({ browserFields, data, + flattenedData, timelineId, }: { browserFields: BrowserFields; data: Ecs; + flattenedData?: TimelineNonEcsData[]; timelineId: string; }) => React.ReactNode; } From f42ba60914ad9a0054ebea194685df24bfe877a3 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 31 Mar 2021 20:37:45 -0500 Subject: [PATCH 05/30] Rewrite existing row renderer in terms of flattened data Updates logic, tests and mocks accordingly. --- .../common/mock/mock_detection_alerts.ts | 9 +++ .../cti/threat_match_row_renderer.test.tsx | 68 +++++++------------ .../cti/threat_match_row_renderer.tsx | 12 ++-- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts index 6d7f48cb9626e9..1495ba9522b65e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts @@ -6,6 +6,7 @@ */ import { Ecs } from '../../../common/ecs'; +import { TimelineNonEcsData } from '../../../common/search_strategy'; export const mockEcsDataWithAlert: Ecs = { _id: '1', @@ -77,3 +78,11 @@ export const getDetectionAlertMock = (overrides: Partial = {}): Ecs => ({ ...mockEcsDataWithAlert, ...overrides, }); + +export const getDetectionAlertFieldsMock = ( + fields: TimelineNonEcsData[] = [] +): TimelineNonEcsData[] => [ + { field: '@timestamp', value: ['2021-03-27T06:28:47.292Z'] }, + { field: 'signal.rule.type', value: ['threat_match'] }, + ...fields, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx index 24078b58b82d53..1bccb7853f461d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx @@ -8,63 +8,46 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { getDetectionAlertMock } from '../../../../../../common/mock'; +import { Ecs } from '../../../../../../../common/ecs'; +import { getDetectionAlertFieldsMock } from '../../../../../../common/mock'; import { threatMatchRowRenderer } from './threat_match_row_renderer'; describe('threatMatchRowRenderer', () => { - let alertMock: ReturnType; + let threatMatchFields: ReturnType; + let ecs: Ecs; beforeEach(() => { - alertMock = getDetectionAlertMock({ - threat: { - indicator: [ - { - matched: { - type: 'url', - }, - }, - ], - }, - }); + ecs = { + _id: 'abcd', + timestamp: '2018-11-12T19:03:25.936Z', + }; + threatMatchFields = getDetectionAlertFieldsMock([ + { field: 'threat.indicator.matched.type', value: ['url'] }, + ]); }); describe('#isInstance', () => { - it('is false for a minimal event', () => { - const minimalEvent = { - _id: 'abcd', - timestamp: '2018-11-12T19:03:25.936Z', - }; - expect(threatMatchRowRenderer.isInstance(minimalEvent)).toBe(false); + it('is false for an empty event', () => { + expect(threatMatchRowRenderer.isInstance(ecs, [])).toBe(false); }); it('is false for an alert with indicator data but no match', () => { - const indicatorEvent = getDetectionAlertMock({ - threat: { - indicator: { - type: 'url', - }, - }, - }); - expect(threatMatchRowRenderer.isInstance(indicatorEvent)).toBe(false); + const indicatorTypeFields = getDetectionAlertFieldsMock([ + { field: 'threat.indicator.type', value: ['url'] }, + ]); + expect(threatMatchRowRenderer.isInstance(ecs, indicatorTypeFields)).toBe(false); }); - it('is true for any event with indicator match fields', () => { - const indicatorMatchEvent = { - _id: 'abc', - threat: { - indicator: { - matched: { - type: 'ip', - }, - }, - }, - }; - expect(threatMatchRowRenderer.isInstance(indicatorMatchEvent)).toBe(true); + it('is false for an alert with threat match fields but no data', () => { + const emptyThreatMatchFields = getDetectionAlertFieldsMock([ + { field: 'threat.indicator.matched.type', value: [] }, + ]); + expect(threatMatchRowRenderer.isInstance(ecs, emptyThreatMatchFields)).toBe(false); }); - it('is true for an alert enriched by an indicator match', () => { - expect(threatMatchRowRenderer.isInstance(alertMock)).toBe(true); + it('is true for an alert event with present indicator match fields', () => { + expect(threatMatchRowRenderer.isInstance(ecs, threatMatchFields)).toBe(true); }); }); @@ -72,7 +55,8 @@ describe('threatMatchRowRenderer', () => { it('renders correctly against snapshot', () => { const children = threatMatchRowRenderer.renderRow({ browserFields: {}, - data: alertMock, + data: ecs, + flattenedData: threatMatchFields, timelineId: 'test', }); const wrapper = shallow({children}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx index 1ca9a8a2de2c8a..9e079f30811d9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx @@ -5,19 +5,17 @@ * 2.0. */ -import { get } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { Ecs } from '../../../../../../../common/ecs'; +import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; import { RowRendererId } from '../../../../../../../common/types/timeline'; -import { asArrayIfExists } from '../../../../../../common/lib/helpers'; import { RowRenderer } from '../row_renderer'; import { ThreatMatchRow } from './threat_match_row'; -const THREAT_INDICATOR_FIELD = 'threat.indicator'; -const isThreatMatch = (ecs: Ecs): boolean => { - const [threatIndicator] = asArrayIfExists(get(THREAT_INDICATOR_FIELD, ecs)) ?? []; - return !!threatIndicator?.matched; -}; +const THREAT_MATCH_FIELD = 'threat.indicator.matched.type'; +const isThreatMatch = (ecs: Ecs, fields: TimelineNonEcsData[] = []): boolean => + fields.some((field) => field.field === THREAT_MATCH_FIELD && !isEmpty(field.value)); export const requiredFields = [ 'threat.indicator.event.dataset', From 10a53f83d0318d985aa76f0926c19967be932b27 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 31 Mar 2021 21:47:06 -0500 Subject: [PATCH 06/30] Moving logic into discrete files * helpers * explicit fields file, which will hopefully be part of the renderer API at some point * parent component to split data into "rows" as defined by our renderer * row component for stateless presentation of a single match --- .../components/alerts_table/index.tsx | 2 +- .../timeline/body/renderers/cti/fields.ts | 19 +++++++++ .../timeline/body/renderers/cti/helpers.ts | 15 +++++++ .../renderers/cti/threat_match_row.test.tsx | 23 ++++------- .../body/renderers/cti/threat_match_row.tsx | 18 ++------- .../cti/threat_match_row_renderer.tsx | 23 +++-------- .../body/renderers/cti/threat_match_rows.tsx | 40 +++++++++++++++++++ 7 files changed, 90 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 8279904704ac3d..7eb4d5cf926f64 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -47,7 +47,7 @@ import { } from '../../../common/components/toasters'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; -import { requiredFields as requiredThreatMatchFields } from '../../../timelines/components/timeline/body/renderers/cti/threat_match_row_renderer'; +import { requiredFields as requiredThreatMatchFields } from '../../../timelines/components/timeline/body/renderers/cti/fields'; import { buildTimeRangeFilter } from './helpers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts new file mode 100644 index 00000000000000..914e40969c883b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const threatMatchFields = [ + 'threat.indicator.matched.atomic', + 'threat.indicator.matched.field', + 'threat.indicator.matched.type', +]; + +export const requiredFields = [ + 'threat.indicator.event.dataset', + 'threat.indicator.event.reference', + 'threat.indicator.provider', + ...threatMatchFields, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts new file mode 100644 index 00000000000000..23300e52066bfe --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; +import { requiredFields, threatMatchFields } from './fields'; + +export const isThreatMatchField = (field: TimelineNonEcsData): boolean => + threatMatchFields.includes(field.field); + +export const isRequiredField = (field: TimelineNonEcsData): boolean => + requiredFields.includes(field.field); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx index 2cb6ef0ae9769a..7a21d82eea047b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -9,37 +9,28 @@ import { mount } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; -import { getDetectionAlertMock } from '../../../../../../common/mock'; +import { getDetectionAlertFieldsMock } from '../../../../../../common/mock'; import { ThreatMatchRow } from './threat_match_row'; describe('threatMatchRow', () => { - let alertMock: ReturnType; + let threatMatchFields: ReturnType; let mockTheme: ReturnType; beforeEach(() => { mockTheme = getMockTheme({ eui: { paddingSizes: {} } }); - alertMock = getDetectionAlertMock({ - threat: { - indicator: [ - { - matched: { - type: 'url', - }, - }, - ], - }, - }); + threatMatchFields = getDetectionAlertFieldsMock([ + { field: 'threat.indicator.matched.type', value: ['url'] }, + ]); }); it('renders an indicator match alert', () => { const wrapper = mount( - + ); - expect(wrapper.find('[data-test-subj="threat-match-row-renderer"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="threat-match-row"]').exists()).toEqual(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 7801fc1ed5b54c..5937a0b9442a95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -6,20 +6,8 @@ */ import React from 'react'; -import { Ecs } from '../../../../../../../common/ecs'; -import { BrowserFields } from '../../../../../../common/containers/source'; -import { RowRendererContainer } from '../row_renderer'; +import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; -export const ThreatMatchRow = ({ - browserFields, - data, - timelineId, -}: { - browserFields: BrowserFields; - data: Ecs; - timelineId: string; -}) => ( - - - +export const ThreatMatchRow = ({ fields }: { fields: TimelineNonEcsData[] }) => ( +
{JSON.stringify(fields, null, 2)}
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx index 9e079f30811d9a..33b318a2de7d1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx @@ -7,27 +7,14 @@ import { isEmpty } from 'lodash/fp'; -import { Ecs } from '../../../../../../../common/ecs'; -import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; import { RowRendererId } from '../../../../../../../common/types/timeline'; import { RowRenderer } from '../row_renderer'; -import { ThreatMatchRow } from './threat_match_row'; - -const THREAT_MATCH_FIELD = 'threat.indicator.matched.type'; -const isThreatMatch = (ecs: Ecs, fields: TimelineNonEcsData[] = []): boolean => - fields.some((field) => field.field === THREAT_MATCH_FIELD && !isEmpty(field.value)); - -export const requiredFields = [ - 'threat.indicator.event.dataset', - 'threat.indicator.event.reference', - 'threat.indicator.provider', - 'threat.indicator.matched.atomic', - 'threat.indicator.matched.field', - 'threat.indicator.matched.type', -]; +import { isThreatMatchField } from './helpers'; +import { ThreatMatchRows } from './threat_match_rows'; export const threatMatchRowRenderer: RowRenderer = { id: RowRendererId.threat_match, - isInstance: isThreatMatch, - renderRow: ThreatMatchRow, + isInstance: (_, fields) => + fields?.some((field) => isThreatMatchField(field) && !isEmpty(field.value)) ?? false, + renderRow: ThreatMatchRows, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx new file mode 100644 index 00000000000000..80100146465fe5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; +import { RowRendererContainer } from '../row_renderer'; +import { isRequiredField, isThreatMatchField } from './helpers'; +import { ThreatMatchRow } from './threat_match_row'; + +const sliceThreatMatchFields = ( + fields: TimelineNonEcsData[], + index: number +): TimelineNonEcsData[] => + fields + .filter((field) => isRequiredField(field)) + .map((field) => ({ + field: field.field, + value: field.value ? [field.value[index]] : undefined, + })); + +export const ThreatMatchRows = ({ flattenedData }: { flattenedData?: TimelineNonEcsData[] }) => { + const threatMatchCount = + flattenedData?.find((field) => isThreatMatchField(field))?.value?.length ?? 0; + const slicedThreatMatchFields = Array.from(Array(threatMatchCount)).map((_, index) => + sliceThreatMatchFields(flattenedData ?? [], index) + ); + + return ( + + {slicedThreatMatchFields?.map((fields, index) => ( + + ))} + + ); +}; From 9d7afd0e95092d4235d685b5c216495f1bf736ec Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 31 Mar 2021 22:04:03 -0500 Subject: [PATCH 07/30] Register threat match row rendere Adds tentative copy, example row, and accompanying mock data. --- .../public/common/mock/mock_timeline_data.ts | 19 +++++++++++++++ .../row_renderers_browser/catalog/index.tsx | 8 +++++++ .../catalog/translations.ts | 13 ++++++++++ .../row_renderers_browser/examples/index.tsx | 1 + .../examples/threat_match.tsx | 24 +++++++++++++++++++ .../timeline/body/renderers/index.ts | 2 ++ 6 files changed, 67 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index f016b6cc34539c..d2a4619b26ad69 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -1088,6 +1088,25 @@ export const mockTimelineData: TimelineItem[] = [ geo: { region_name: ['xx'], country_iso_code: ['xx'] }, }, }, + { + _id: '32', + data: [ + // TODO use more realistic data + { field: 'threat.indicator.matched.atomic', value: ['laptop.local'] }, + { field: 'threat.indicator.matched.field', value: ['host.name'] }, + { field: 'threat.indicator.matched.type', value: ['domain'] }, + { field: 'threat.indicator.event.dataset', value: ['threatintel.abuseurl'] }, + { + field: 'threat.indicator.event.reference', + value: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { field: 'threat.indicator.provider', value: ['indicator_provider'] }, + ], + ecs: { + _id: 'BuBP4W0BOpWiDweSoYSg', + timestamp: '2019-10-18T23:59:15.091Z', + }, + }, ]; export const mockFimFileCreatedEvent: Ecs = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx index 283a239acad24b..f724c19913c8e8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -24,6 +24,7 @@ import { SystemFimExample, SystemSecurityEventExample, SystemSocketExample, + ThreatMatchExample, ZeekExample, } from '../examples'; import * as i18n from './translations'; @@ -204,6 +205,13 @@ export const renderers: RowRendererOption[] = [ example: SuricataExample, searchableDescription: `${i18n.SURICATA_DESCRIPTION_PART1} ${i18n.SURICATA_NAME} ${i18n.SURICATA_DESCRIPTION_PART2}`, }, + { + id: RowRendererId.threat_match, + name: i18n.THREAT_MATCH_NAME, + description: i18n.THREAT_MATCH_DESCRIPTION, + example: ThreatMatchExample, + searchableDescription: `${i18n.THREAT_MATCH_NAME} ${i18n.THREAT_MATCH_DESCRIPTION}`, + }, { id: RowRendererId.zeek, name: i18n.ZEEK_NAME, diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts index a0d6d4e121891f..95dce2e96d1861 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts @@ -230,6 +230,19 @@ export const SYSTEM_DESCRIPTION_PART3 = i18n.translate( 'All datasets send both periodic state information (e.g. all currently running processes) and real-time changes (e.g. when a new process starts or stops).', } ); +export const THREAT_MATCH_NAME = i18n.translate( + 'xpack.securitySolution.eventRenderers.threatMatchName', + { + defaultMessage: 'Threat Indicator Match', + } +); + +export const THREAT_MATCH_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.eventRenderers.threatMatchDescription', + { + defaultMessage: 'Summarizes events that matched threat indicators', + } +); export const ZEEK_NAME = i18n.translate('xpack.securitySolution.eventRenderers.zeekName', { defaultMessage: 'Zeek (formerly Bro)', diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx index 6932ca01835cc0..da9d6923c2b76b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx @@ -19,4 +19,5 @@ export * from './system_file'; export * from './system_fim'; export * from './system_security_event'; export * from './system_socket'; +export * from './threat_match'; export * from './zeek'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx new file mode 100644 index 00000000000000..81854563c4d14d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { threatMatchRowRenderer } from '../../timeline/body/renderers/cti/threat_match_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; + +const ThreatMatchExampleComponent: React.FC = () => ( + <> + {threatMatchRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[31].ecs, + flattenedData: mockTimelineData[31].data, + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, + })} + +); +export const ThreatMatchExample = React.memo(ThreatMatchExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 209a9414f62f18..537a24bbfd9539 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -15,6 +15,7 @@ import { suricataRowRenderer } from './suricata/suricata_row_renderer'; import { unknownColumnRenderer } from './unknown_column_renderer'; import { zeekRowRenderer } from './zeek/zeek_row_renderer'; import { systemRowRenderers } from './system/generic_row_renderer'; +import { threatMatchRowRenderer } from './cti/threat_match_row_renderer'; // The row renderers are order dependent and will return the first renderer // which returns true from its isInstance call. The bottom renderers which @@ -24,6 +25,7 @@ import { systemRowRenderers } from './system/generic_row_renderer'; // plainRowRenderer always returns true to everything which is why it always // should be last. export const defaultRowRenderers: RowRenderer[] = [ + threatMatchRowRenderer, ...auditdRowRenderers, ...systemRowRenderers, suricataRowRenderer, From adf7067ad822b4a3dcffefa6d02b44b90d7ecae2 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 1 Apr 2021 20:27:11 -0500 Subject: [PATCH 08/30] WIP: Rendering draggable fields but hit the data loss issue with nested fields being flattened --- .../body/renderers/cti/threat_match_row.tsx | 23 ++++++++++++++++++- .../body/renderers/cti/threat_match_rows.tsx | 12 +++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 5937a0b9442a95..ec26ad0cf90537 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -5,9 +5,30 @@ * 2.0. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; + import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; export const ThreatMatchRow = ({ fields }: { fields: TimelineNonEcsData[] }) => ( -
{JSON.stringify(fields, null, 2)}
+ + {fields.map((field) => ( + + + + ))} + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 80100146465fe5..6481df7395e042 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -24,15 +24,21 @@ const sliceThreatMatchFields = ( })); export const ThreatMatchRows = ({ flattenedData }: { flattenedData?: TimelineNonEcsData[] }) => { - const threatMatchCount = - flattenedData?.find((field) => isThreatMatchField(field))?.value?.length ?? 0; - const slicedThreatMatchFields = Array.from(Array(threatMatchCount)).map((_, index) => + // TODO this code sucks and we should get rid of it + // where do our threat fields get flattened like this? it's not in the fields response itself + // it comes from the backend, so it must be the search strategy + + const threatMatchCount = flattenedData?.find((field) => isThreatMatchField(field))?.value; + const t = threatMatchCount?.length ?? 0; + console.log('threatMatchCount', threatMatchCount); + const slicedThreatMatchFields = Array.from(Array(t)).map((_, index) => sliceThreatMatchFields(flattenedData ?? [], index) ); return ( {slicedThreatMatchFields?.map((fields, index) => ( + // TODO object with keys instead of fields ))} From 6fc1d90e81ec5c00bcecc40011ceeaa47863ad78 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 5 Apr 2021 22:10:08 -0500 Subject: [PATCH 09/30] WIP: implementing row renderer against new data format I haven't yet deleted the old (new?) unused path yet. Cleanup to come. --- .../matrix_histogram/events/index.ts | 1 + .../threat_match_row.test.tsx.snap | 91 ++++++++++++ .../renderers/cti/threat_match_row.test.tsx | 67 +++++++-- .../body/renderers/cti/threat_match_row.tsx | 134 +++++++++++++++--- .../body/renderers/cti/threat_match_rows.tsx | 38 ++--- 5 files changed, 270 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts index b2e0461b0b9b8a..316b4a95d47f6f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts @@ -27,6 +27,7 @@ export interface EventsActionGroupData { } export type Fields = Record; +export type ParsedFields = Record; export interface EventHit extends SearchHit { sort: string[]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap new file mode 100644 index 00000000000000..b6cafa841c6f9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`threatMatchRowView matches the registered snapshot 1`] = ` + + + + + + indicator matched on + + + + , whose value was + + + + + + + + + via + + + + : + + + + + +`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx index 7a21d82eea047b..b5811ee544a51c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -5,32 +5,73 @@ * 2.0. */ -import { mount } from 'enzyme'; +import { shallow } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; -import { getDetectionAlertFieldsMock } from '../../../../../../common/mock'; -import { ThreatMatchRow } from './threat_match_row'; +import { getDetectionAlertFieldsMock, TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +import { ThreatMatchRowView } from './threat_match_row'; -describe('threatMatchRow', () => { +describe('threatMatchRowView', () => { + const mount = useMountAppended(); let threatMatchFields: ReturnType; - let mockTheme: ReturnType; beforeEach(() => { - mockTheme = getMockTheme({ eui: { paddingSizes: {} } }); threatMatchFields = getDetectionAlertFieldsMock([ { field: 'threat.indicator.matched.type', value: ['url'] }, ]); }); - it('renders an indicator match alert', () => { - const wrapper = mount( - - - + it('renders an indicator match row', () => { + const wrapper = shallow( + ); expect(wrapper.find('[data-test-subj="threat-match-row"]').exists()).toEqual(true); }); + + it('matches the registered snapshot', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders draggable fields', () => { + const wrapper = mount( + + + + ); + + const sourceValueDraggable = wrapper.find('[data-test-subj="threat-match-row-source-value"]'); + expect(sourceValueDraggable.props()).toEqual( + expect.objectContaining({ + field: 'host.name', + value: 'http://elastic.co', + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index ec26ad0cf90537..779f488ab1ef90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -5,30 +5,120 @@ * 2.0. */ +import { get } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; +import { ParsedFields } from '../../../../../../../common/search_strategy'; import { DraggableBadge } from '../../../../../../common/components/draggables'; -export const ThreatMatchRow = ({ fields }: { fields: TimelineNonEcsData[] }) => ( - - {fields.map((field) => ( - - - - ))} - -); +export interface ThreatMatchRowProps { + indicatorDataset: string; + indicatorProvider: string; + indicatorReference: string; + indicatorType: string; + sourceField: string; + sourceValue: string; +} + +const ThreatMatchRowContainer = ({ fields }: { fields: ParsedFields }) => { + const props = { + indicatorDataset: get(fields, 'event.dataset')[0] as string, + indicatorReference: get(fields, 'event.reference')[0] as string, + indicatorProvider: get(fields, 'provider')[0] as string, + indicatorType: get(fields, 'matched.type')[0] as string, + sourceField: get(fields, 'matched.field')[0] as string, + sourceValue: get(fields, 'matched.atomic')[0] as string, + }; + + return ; +}; + +export const ThreatMatchRowView = ({ + indicatorDataset, + indicatorProvider, + indicatorReference, + indicatorType, + sourceField, + sourceValue, +}: ThreatMatchRowProps) => { + return ( + <> + + + + + {'indicator matched on'} + + + + {', whose value was'} + + + + + + + + + {'via'} + + + + {':'} + + + + + + ); +}; + +export { ThreatMatchRowContainer as ThreatMatchRow }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 6481df7395e042..5a02424d4e8b7a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -5,40 +5,26 @@ * 2.0. */ +import { get } from 'lodash'; import React from 'react'; -import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; +import { Ecs } from '../../../../../../../common/ecs'; +import { ParsedFields, TimelineNonEcsData } from '../../../../../../../common/search_strategy'; import { RowRendererContainer } from '../row_renderer'; -import { isRequiredField, isThreatMatchField } from './helpers'; import { ThreatMatchRow } from './threat_match_row'; -const sliceThreatMatchFields = ( - fields: TimelineNonEcsData[], - index: number -): TimelineNonEcsData[] => - fields - .filter((field) => isRequiredField(field)) - .map((field) => ({ - field: field.field, - value: field.value ? [field.value[index]] : undefined, - })); - -export const ThreatMatchRows = ({ flattenedData }: { flattenedData?: TimelineNonEcsData[] }) => { - // TODO this code sucks and we should get rid of it - // where do our threat fields get flattened like this? it's not in the fields response itself - // it comes from the backend, so it must be the search strategy - - const threatMatchCount = flattenedData?.find((field) => isThreatMatchField(field))?.value; - const t = threatMatchCount?.length ?? 0; - console.log('threatMatchCount', threatMatchCount); - const slicedThreatMatchFields = Array.from(Array(t)).map((_, index) => - sliceThreatMatchFields(flattenedData ?? [], index) - ); +export const ThreatMatchRows = ({ + data, + flattenedData, +}: { + data: Ecs; + flattenedData?: TimelineNonEcsData[]; +}) => { + const indicators = get(data, 'threat.indicator') as ParsedFields[]; return ( - {slicedThreatMatchFields?.map((fields, index) => ( - // TODO object with keys instead of fields + {indicators.map((fields, index) => ( ))} From c2eb905f8101bc1221e92c02e7aa986cb050bbf5 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 5 Apr 2021 23:09:09 -0500 Subject: [PATCH 10/30] Updating based on new data * Rewrites isInstance logic for new data as helper, hasThreatMatchValue * Updating types and tests * Adds to the previously empty ThreatEcs --- .../common/ecs/rule/index.ts | 2 +- .../common/ecs/threat/index.ts | 16 ++++++- .../common/mock/mock_detection_alerts.ts | 24 ++++++++++ .../components/alerts_table/index.tsx | 2 +- .../threat_match_row_renderer.test.tsx.snap | 19 +++++++- .../timeline/body/renderers/cti/constants.ts | 15 +++++++ .../timeline/body/renderers/cti/fields.ts | 19 -------- .../timeline/body/renderers/cti/helpers.ts | 20 ++++++--- .../renderers/cti/threat_match_row.test.tsx | 9 +--- .../cti/threat_match_row_renderer.test.tsx | 45 +++++++++---------- .../cti/threat_match_row_renderer.tsx | 7 +-- 11 files changed, 112 insertions(+), 66 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts diff --git a/x-pack/plugins/security_solution/common/ecs/rule/index.ts b/x-pack/plugins/security_solution/common/ecs/rule/index.ts index 5463b21f6b7f70..ae7e5064a8eced 100644 --- a/x-pack/plugins/security_solution/common/ecs/rule/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/rule/index.ts @@ -9,7 +9,7 @@ export interface RuleEcs { id?: string[]; rule_id?: string[]; name?: string[]; - false_positives: string[]; + false_positives?: string[]; saved_id?: string[]; timeline_id?: string[]; timeline_title?: string[]; diff --git a/x-pack/plugins/security_solution/common/ecs/threat/index.ts b/x-pack/plugins/security_solution/common/ecs/threat/index.ts index 697ce37d9eb9fb..9273c01e13f697 100644 --- a/x-pack/plugins/security_solution/common/ecs/threat/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/threat/index.ts @@ -5,5 +5,17 @@ * 2.0. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ThreatEcs {} +interface ThreatMatchEcs { + atomic?: string[]; + field?: string[]; + type?: string[]; +} + +export interface ThreatIndicatorEcs { + matched?: ThreatMatchEcs; + type?: string[]; +} + +export interface ThreatEcs { + indicator: ThreatIndicatorEcs[]; +} diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts index 1495ba9522b65e..2d93e7e0dc3a73 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detection_alerts.ts @@ -79,6 +79,30 @@ export const getDetectionAlertMock = (overrides: Partial = {}): Ecs => ({ ...overrides, }); +export const getThreatMatchDetectionAlert = (overrides: Partial = {}): Ecs => ({ + ...mockEcsDataWithAlert, + signal: { + ...mockEcsDataWithAlert.signal, + rule: { + ...mockEcsDataWithAlert.rule, + name: ['mock threat_match rule'], + type: ['threat_match'], + }, + }, + threat: { + indicator: [ + { + matched: { + atomic: ['matched.atomic'], + field: ['matched.atomic'], + type: ['matched.domain'], + }, + }, + ], + }, + ...overrides, +}); + export const getDetectionAlertFieldsMock = ( fields: TimelineNonEcsData[] = [] ): TimelineNonEcsData[] => [ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 7eb4d5cf926f64..88430f8165f018 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -47,7 +47,7 @@ import { } from '../../../common/components/toasters'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; -import { requiredFields as requiredThreatMatchFields } from '../../../timelines/components/timeline/body/renderers/cti/fields'; +import { requiredFields as requiredThreatMatchFields } from '../../../timelines/components/timeline/body/renderers/cti/constants'; import { buildTimeRangeFilter } from './helpers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap index 4ced3b51c7a7f4..b7d6c9d7272185 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap @@ -5,7 +5,24 @@ exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1` - + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts new file mode 100644 index 00000000000000..feacc02df4d35b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INDICATOR_DESTINATION_PATH } from '../../../../../../../common/constants'; + +export const threatMatchSubFields = ['matched.atomic', 'matched.field', 'matched.type']; +const indicatorSubFields = ['event.dataset', 'event.reference', 'provider']; + +export const requiredFields = [...threatMatchSubFields, ...indicatorSubFields].map( + (subField) => `${INDICATOR_DESTINATION_PATH}.${subField}` +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts deleted file mode 100644 index 914e40969c883b..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/fields.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const threatMatchFields = [ - 'threat.indicator.matched.atomic', - 'threat.indicator.matched.field', - 'threat.indicator.matched.type', -]; - -export const requiredFields = [ - 'threat.indicator.event.dataset', - 'threat.indicator.event.reference', - 'threat.indicator.provider', - ...threatMatchFields, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts index 23300e52066bfe..88132077dc2195 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts @@ -5,11 +5,19 @@ * 2.0. */ -import { TimelineNonEcsData } from '../../../../../../../common/search_strategy'; -import { requiredFields, threatMatchFields } from './fields'; +import { get, isEmpty } from 'lodash'; -export const isThreatMatchField = (field: TimelineNonEcsData): boolean => - threatMatchFields.includes(field.field); +import { INDICATOR_DESTINATION_PATH } from '../../../../../../../common/constants'; +import { Ecs } from '../../../../../../../common/ecs'; +import { ThreatIndicatorEcs } from '../../../../../../../common/ecs/threat'; +import { threatMatchSubFields } from './constants'; -export const isRequiredField = (field: TimelineNonEcsData): boolean => - requiredFields.includes(field.field); +export const getIndicatorEcs = (data: Ecs): ThreatIndicatorEcs[] => + get(data, INDICATOR_DESTINATION_PATH) ?? []; + +export const hasThreatMatchValue = (data: Ecs): boolean => + getIndicatorEcs(data).some((indicator) => + threatMatchSubFields.some( + (threatMatchSubField) => !isEmpty(get(indicator, threatMatchSubField)) + ) + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx index b5811ee544a51c..ac2ef9f559736a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -8,19 +8,12 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { getDetectionAlertFieldsMock, TestProviders } from '../../../../../../common/mock'; +import { TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ThreatMatchRowView } from './threat_match_row'; describe('threatMatchRowView', () => { const mount = useMountAppended(); - let threatMatchFields: ReturnType; - - beforeEach(() => { - threatMatchFields = getDetectionAlertFieldsMock([ - { field: 'threat.indicator.matched.type', value: ['url'] }, - ]); - }); it('renders an indicator match row', () => { const wrapper = shallow( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx index 1bccb7853f461d..6687179e5b8878 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.test.tsx @@ -8,46 +8,46 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Ecs } from '../../../../../../../common/ecs'; -import { getDetectionAlertFieldsMock } from '../../../../../../common/mock'; +import { getThreatMatchDetectionAlert } from '../../../../../../common/mock'; import { threatMatchRowRenderer } from './threat_match_row_renderer'; describe('threatMatchRowRenderer', () => { - let threatMatchFields: ReturnType; - let ecs: Ecs; + let threatMatchData: ReturnType; beforeEach(() => { - ecs = { - _id: 'abcd', - timestamp: '2018-11-12T19:03:25.936Z', - }; - threatMatchFields = getDetectionAlertFieldsMock([ - { field: 'threat.indicator.matched.type', value: ['url'] }, - ]); + threatMatchData = getThreatMatchDetectionAlert(); }); describe('#isInstance', () => { it('is false for an empty event', () => { - expect(threatMatchRowRenderer.isInstance(ecs, [])).toBe(false); + const emptyEvent = { + _id: 'my_id', + '@timestamp': ['2020-11-17T14:48:08.922Z'], + }; + expect(threatMatchRowRenderer.isInstance(emptyEvent)).toBe(false); }); it('is false for an alert with indicator data but no match', () => { - const indicatorTypeFields = getDetectionAlertFieldsMock([ - { field: 'threat.indicator.type', value: ['url'] }, - ]); - expect(threatMatchRowRenderer.isInstance(ecs, indicatorTypeFields)).toBe(false); + const indicatorTypeData = getThreatMatchDetectionAlert({ + threat: { + indicator: [{ type: ['url'] }], + }, + }); + expect(threatMatchRowRenderer.isInstance(indicatorTypeData)).toBe(false); }); it('is false for an alert with threat match fields but no data', () => { - const emptyThreatMatchFields = getDetectionAlertFieldsMock([ - { field: 'threat.indicator.matched.type', value: [] }, - ]); - expect(threatMatchRowRenderer.isInstance(ecs, emptyThreatMatchFields)).toBe(false); + const emptyThreatMatchData = getThreatMatchDetectionAlert({ + threat: { + indicator: [{ matched: { type: [] } }], + }, + }); + expect(threatMatchRowRenderer.isInstance(emptyThreatMatchData)).toBe(false); }); it('is true for an alert event with present indicator match fields', () => { - expect(threatMatchRowRenderer.isInstance(ecs, threatMatchFields)).toBe(true); + expect(threatMatchRowRenderer.isInstance(threatMatchData)).toBe(true); }); }); @@ -55,8 +55,7 @@ describe('threatMatchRowRenderer', () => { it('renders correctly against snapshot', () => { const children = threatMatchRowRenderer.renderRow({ browserFields: {}, - data: ecs, - flattenedData: threatMatchFields, + data: threatMatchData, timelineId: 'test', }); const wrapper = shallow({children}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx index 33b318a2de7d1b..2a7e8ce02d79f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx @@ -5,16 +5,13 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; - import { RowRendererId } from '../../../../../../../common/types/timeline'; import { RowRenderer } from '../row_renderer'; -import { isThreatMatchField } from './helpers'; +import { hasThreatMatchValue } from './helpers'; import { ThreatMatchRows } from './threat_match_rows'; export const threatMatchRowRenderer: RowRenderer = { id: RowRendererId.threat_match, - isInstance: (_, fields) => - fields?.some((field) => isThreatMatchField(field) && !isEmpty(field.value)) ?? false, + isInstance: hasThreatMatchValue, renderRow: ThreatMatchRows, }; From b20b71b57da2312ca022c825e17423c047e4e7cf Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 5 Apr 2021 23:22:18 -0500 Subject: [PATCH 11/30] Revert "Pass full fields data to our row renderers" This reverts commit 19c93ee0732166747b5472433cd5fc813638e21b. We ended up extending the existing data (albeit from the fields response!). --- .../timeline/body/events/stateful_event.tsx | 8 +-- .../events/stateful_row_renderer/index.tsx | 5 +- .../body/renderers/get_row_renderer.test.tsx | 59 ++++++++++--------- .../body/renderers/get_row_renderer.ts | 9 +-- .../timeline/body/renderers/row_renderer.tsx | 5 +- 5 files changed, 38 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 019bf2ea3dd72e..97ab088b615833 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -148,10 +148,10 @@ const StatefulEventComponent: React.FC = ({ [notesById, noteIds] ); - const hasRowRenderers: boolean = useMemo( - () => getRowRenderer(rowRenderers, event.ecs, event.data) != null, - [event.ecs, event.data, rowRenderers] - ); + const hasRowRenderers: boolean = useMemo(() => getRowRenderer(event.ecs, rowRenderers) != null, [ + event.ecs, + rowRenderers, + ]); const onToggleShowNotes = useCallback(() => { const eventId = event._id; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx index 50aa638d1df1c0..10a25538c1ba39 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx @@ -61,9 +61,8 @@ export const StatefulRowRenderer = ({ rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, }); - const rowRenderer = useMemo(() => getRowRenderer(rowRenderers, event.ecs, event.data), [ + const rowRenderer = useMemo(() => getRowRenderer(event.ecs, rowRenderers), [ event.ecs, - event.data, rowRenderers, ]); @@ -81,7 +80,6 @@ export const StatefulRowRenderer = ({ {rowRenderer.renderRow({ browserFields, data: event.ecs, - flattenedData: event.data, timelineId, })} @@ -93,7 +91,6 @@ export const StatefulRowRenderer = ({ ariaRowindex, browserFields, event.ecs, - event.data, focusOwnership, onFocus, onKeyDown, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index c3c3c5e6b74128..b92a4381d837b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -6,13 +6,14 @@ */ import { shallow } from 'enzyme'; +import { cloneDeep } from 'lodash'; import React from 'react'; import { removeExternalLinkText } from '../../../../../../common/test_utils'; import '../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../../common/ecs'; import { mockTimelineData } from '../../../../../common/mock'; -import { TimelineItem } from '../../../../../../common/search_strategy'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; @@ -31,26 +32,26 @@ jest.mock('@elastic/eui', () => { jest.mock('../../../../../common/components/link_to'); describe('get_column_renderer', () => { - let nonSuricata: TimelineItem; - let suricata: TimelineItem; - let zeek: TimelineItem; - let system: TimelineItem; - let auditd: TimelineItem; + let nonSuricata: Ecs; + let suricata: Ecs; + let zeek: Ecs; + let system: Ecs; + let auditd: Ecs; const mount = useMountAppended(); beforeEach(() => { - nonSuricata = mockTimelineData[0]; - suricata = mockTimelineData[2]; - zeek = mockTimelineData[13]; - system = mockTimelineData[28]; - auditd = mockTimelineData[19]; + nonSuricata = cloneDeep(mockTimelineData[0].ecs); + suricata = cloneDeep(mockTimelineData[2].ecs); + zeek = cloneDeep(mockTimelineData[13].ecs); + system = cloneDeep(mockTimelineData[28].ecs); + auditd = cloneDeep(mockTimelineData[19].ecs); }); test('renders correctly against snapshot', () => { - const rowRenderer = getRowRenderer(defaultRowRenderers, nonSuricata.ecs, nonSuricata.data); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: nonSuricata.ecs, + data: nonSuricata, timelineId: 'test', }); @@ -59,10 +60,10 @@ describe('get_column_renderer', () => { }); test('should render plain row data when it is a non suricata row', () => { - const rowRenderer = getRowRenderer(defaultRowRenderers, nonSuricata.ecs, nonSuricata.data); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: nonSuricata.ecs, + data: nonSuricata, timelineId: 'test', }); const wrapper = mount( @@ -74,10 +75,10 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data when it is a suricata row', () => { - const rowRenderer = getRowRenderer(defaultRowRenderers, suricata.ecs, suricata.data); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: suricata.ecs, + data: suricata, timelineId: 'test', }); const wrapper = mount( @@ -91,11 +92,11 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data if event.category is network_traffic', () => { - suricata.ecs.event = { ...suricata.ecs.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(defaultRowRenderers, suricata.ecs, suricata.data); + suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: suricata.ecs, + data: suricata, timelineId: 'test', }); const wrapper = mount( @@ -109,11 +110,11 @@ describe('get_column_renderer', () => { }); test('should render a zeek row data if event.category is network_traffic', () => { - zeek.ecs.event = { ...zeek.ecs.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(defaultRowRenderers, zeek.ecs, zeek.data); + zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(zeek, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: zeek.ecs, + data: zeek, timelineId: 'test', }); const wrapper = mount( @@ -127,11 +128,11 @@ describe('get_column_renderer', () => { }); test('should render a system row data if event.category is network_traffic', () => { - system.ecs.event = { ...system.ecs.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(defaultRowRenderers, system.ecs, system.data); + system.event = { ...system.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(system, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: system.ecs, + data: system, timelineId: 'test', }); const wrapper = mount( @@ -145,11 +146,11 @@ describe('get_column_renderer', () => { }); test('should render a auditd row data if event.category is network_traffic', () => { - auditd.ecs.event = { ...auditd.ecs.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(defaultRowRenderers, auditd.ecs, auditd.data); + auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(auditd, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, - data: auditd.ecs, + data: auditd, timelineId: 'test', }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts index 875721af4b0821..bfe60a14e042de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts @@ -6,12 +6,7 @@ */ import { Ecs } from '../../../../../../common/ecs'; -import { TimelineNonEcsData } from '../../../../../../common/search_strategy'; import { RowRenderer } from './row_renderer'; -export const getRowRenderer = ( - rowRenderers: RowRenderer[], - ecs: Ecs, - data: TimelineNonEcsData[] -): RowRenderer | null => - rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs, data)) ?? null; +export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null => + rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx index ed4b5be2b2adca..679da28e622bfb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx @@ -11,7 +11,6 @@ import { BrowserFields } from '../../../../../common/containers/source'; import type { RowRendererId } from '../../../../../../common/types/timeline'; import { Ecs } from '../../../../../../common/ecs'; import { EventsTrSupplement } from '../../styles'; -import { TimelineNonEcsData } from '../../../../../../common/search_strategy'; interface RowRendererContainerProps { children: React.ReactNode; @@ -26,16 +25,14 @@ RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { id: RowRendererId; - isInstance: (data: Ecs, flattenedData?: TimelineNonEcsData[]) => boolean; + isInstance: (data: Ecs) => boolean; renderRow: ({ browserFields, data, - flattenedData, timelineId, }: { browserFields: BrowserFields; data: Ecs; - flattenedData?: TimelineNonEcsData[]; timelineId: string; }) => React.ReactNode; } From 39f18800b8051b7cfff474677d96f506a295bc5d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 11 Apr 2021 19:31:50 -0500 Subject: [PATCH 12/30] Fix draggables * adds contextId and eventId to pass to draggable * We don't have a order-independent key for each individual ThreatMatchRow, due to matched.id not being mapped/returned in the fields response * Fixes up a few things related to using the new data format --- .../matrix_histogram/events/index.ts | 5 +- .../body/renderers/cti/threat_match_row.tsx | 56 +++++++++++-------- .../body/renderers/cti/threat_match_rows.tsx | 27 ++++----- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts index 316b4a95d47f6f..4df376acb256ea 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts @@ -26,8 +26,9 @@ export interface EventsActionGroupData { doc_count: number; } -export type Fields = Record; -export type ParsedFields = Record; +export interface Fields { + [x: string]: T | Array>; +} export interface EventHit extends SearchHit { sort: string[]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 779f488ab1ef90..4c366cf3314e3e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -9,10 +9,12 @@ import { get } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { ParsedFields } from '../../../../../../../common/search_strategy'; +import { Fields } from '../../../../../../../common/search_strategy'; import { DraggableBadge } from '../../../../../../common/components/draggables'; export interface ThreatMatchRowProps { + contextId: string; + eventId: string; indicatorDataset: string; indicatorProvider: string; indicatorReference: string; @@ -21,20 +23,32 @@ export interface ThreatMatchRowProps { sourceValue: string; } -const ThreatMatchRowContainer = ({ fields }: { fields: ParsedFields }) => { +export const ThreatMatchRow = ({ + data, + eventId, + timelineId, +}: { + data: Fields; + eventId: string; + timelineId: string; +}) => { const props = { - indicatorDataset: get(fields, 'event.dataset')[0] as string, - indicatorReference: get(fields, 'event.reference')[0] as string, - indicatorProvider: get(fields, 'provider')[0] as string, - indicatorType: get(fields, 'matched.type')[0] as string, - sourceField: get(fields, 'matched.field')[0] as string, - sourceValue: get(fields, 'matched.atomic')[0] as string, + contextId: `threat-match-row-${timelineId}-${eventId}`, + eventId, + indicatorDataset: get(data, 'event.dataset')[0] as string, + indicatorReference: get(data, 'event.reference')[0] as string, + indicatorProvider: get(data, 'provider')[0] as string, + indicatorType: get(data, 'matched.type')[0] as string, + sourceField: get(data, 'matched.field')[0] as string, + sourceValue: get(data, 'matched.atomic')[0] as string, }; return ; }; export const ThreatMatchRowView = ({ + contextId, + eventId, indicatorDataset, indicatorProvider, indicatorReference, @@ -52,9 +66,9 @@ export const ThreatMatchRowView = ({ > @@ -62,9 +76,9 @@ export const ThreatMatchRowView = ({ {'indicator matched on'} @@ -72,9 +86,9 @@ export const ThreatMatchRowView = ({ {', whose value was'} @@ -89,9 +103,9 @@ export const ThreatMatchRowView = ({ > @@ -99,9 +113,9 @@ export const ThreatMatchRowView = ({ {'via'} @@ -109,9 +123,9 @@ export const ThreatMatchRowView = ({ {':'} @@ -120,5 +134,3 @@ export const ThreatMatchRowView = ({ ); }; - -export { ThreatMatchRowContainer as ThreatMatchRow }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 5a02424d4e8b7a..c6198aa6e39e3c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -8,24 +8,25 @@ import { get } from 'lodash'; import React from 'react'; -import { Ecs } from '../../../../../../../common/ecs'; -import { ParsedFields, TimelineNonEcsData } from '../../../../../../../common/search_strategy'; -import { RowRendererContainer } from '../row_renderer'; +import { Fields } from '../../../../../../../common/search_strategy'; +import { ID_FIELD_NAME } from '../../../../../../common/components/event_details/event_id'; +import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { ThreatMatchRow } from './threat_match_row'; -export const ThreatMatchRows = ({ - data, - flattenedData, -}: { - data: Ecs; - flattenedData?: TimelineNonEcsData[]; -}) => { - const indicators = get(data, 'threat.indicator') as ParsedFields[]; +export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) => { + const indicators = get(data, 'threat.indicator') as Fields[]; + const eventId = get(data, ID_FIELD_NAME); return ( - {indicators.map((fields, index) => ( - + {indicators.map((indicator, index) => ( + ))} ); From 3477d27431be2692f566950211a1d4e789ad9f5c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 11 Apr 2021 19:59:07 -0500 Subject: [PATCH 13/30] Move indicator field strings to constants --- .../timeline/body/renderers/cti/constants.ts | 29 +++++++++++--- .../timeline/body/renderers/cti/helpers.ts | 2 +- .../body/renderers/cti/threat_match_row.tsx | 38 +++++++++++++------ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts index feacc02df4d35b..2d81122519ae03 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts @@ -7,9 +7,28 @@ import { INDICATOR_DESTINATION_PATH } from '../../../../../../../common/constants'; -export const threatMatchSubFields = ['matched.atomic', 'matched.field', 'matched.type']; -const indicatorSubFields = ['event.dataset', 'event.reference', 'provider']; +export const MATCHED_ATOMIC = 'matched.atomic'; +export const MATCHED_FIELD = 'matched.field'; +export const MATCHED_TYPE = 'matched.type'; +export const threatMatchSubFields = [MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE]; -export const requiredFields = [...threatMatchSubFields, ...indicatorSubFields].map( - (subField) => `${INDICATOR_DESTINATION_PATH}.${subField}` -); +export const INDICATOR_MATCHED_ATOMIC = `${INDICATOR_DESTINATION_PATH}.${MATCHED_ATOMIC}`; +export const INDICATOR_MATCHED_FIELD = `${INDICATOR_DESTINATION_PATH}.${MATCHED_FIELD}`; +export const INDICATOR_MATCHED_TYPE = `${INDICATOR_DESTINATION_PATH}.${MATCHED_TYPE}`; + +export const EVENT_DATASET = 'event.dataset'; +export const EVENT_REFERENCE = 'event.reference'; +export const PROVIDER = 'provider'; + +export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET}`; +export const INDICATOR_REFERENCE = `${INDICATOR_DESTINATION_PATH}.${EVENT_REFERENCE}`; +export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`; + +export const requiredFields = [ + INDICATOR_MATCHED_ATOMIC, + INDICATOR_MATCHED_FIELD, + INDICATOR_MATCHED_TYPE, + INDICATOR_DATASET, + INDICATOR_REFERENCE, + INDICATOR_PROVIDER, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts index 88132077dc2195..1c9cf5f5784449 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts @@ -12,7 +12,7 @@ import { Ecs } from '../../../../../../../common/ecs'; import { ThreatIndicatorEcs } from '../../../../../../../common/ecs/threat'; import { threatMatchSubFields } from './constants'; -export const getIndicatorEcs = (data: Ecs): ThreatIndicatorEcs[] => +const getIndicatorEcs = (data: Ecs): ThreatIndicatorEcs[] => get(data, INDICATOR_DESTINATION_PATH) ?? []; export const hasThreatMatchValue = (data: Ecs): boolean => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 4c366cf3314e3e..90abe12263de07 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -11,6 +11,19 @@ import React from 'react'; import { Fields } from '../../../../../../../common/search_strategy'; import { DraggableBadge } from '../../../../../../common/components/draggables'; +import { + EVENT_DATASET, + EVENT_REFERENCE, + INDICATOR_DATASET, + INDICATOR_MATCHED_FIELD, + INDICATOR_MATCHED_TYPE, + INDICATOR_PROVIDER, + INDICATOR_REFERENCE, + MATCHED_ATOMIC, + MATCHED_FIELD, + MATCHED_TYPE, + PROVIDER, +} from './constants'; export interface ThreatMatchRowProps { contextId: string; @@ -35,12 +48,12 @@ export const ThreatMatchRow = ({ const props = { contextId: `threat-match-row-${timelineId}-${eventId}`, eventId, - indicatorDataset: get(data, 'event.dataset')[0] as string, - indicatorReference: get(data, 'event.reference')[0] as string, - indicatorProvider: get(data, 'provider')[0] as string, - indicatorType: get(data, 'matched.type')[0] as string, - sourceField: get(data, 'matched.field')[0] as string, - sourceValue: get(data, 'matched.atomic')[0] as string, + indicatorDataset: get(data, EVENT_DATASET)[0] as string, + indicatorReference: get(data, EVENT_REFERENCE)[0] as string, + indicatorProvider: get(data, PROVIDER)[0] as string, + indicatorType: get(data, MATCHED_TYPE)[0] as string, + sourceField: get(data, MATCHED_FIELD)[0] as string, + sourceValue: get(data, MATCHED_ATOMIC)[0] as string, }; return ; @@ -60,6 +73,7 @@ export const ThreatMatchRowView = ({ <> @@ -79,7 +93,7 @@ export const ThreatMatchRowView = ({ contextId={contextId} data-test-subj="threat-match-row-source-field" eventId={eventId} - field={'threat.indicator.matched.field'} + field={INDICATOR_MATCHED_FIELD} value={sourceField} /> @@ -96,7 +110,7 @@ export const ThreatMatchRowView = ({ @@ -116,7 +130,7 @@ export const ThreatMatchRowView = ({ contextId={contextId} data-test-subj="threat-match-row-indicator-provider" eventId={eventId} - field={'threat.indicator.provider'} + field={INDICATOR_PROVIDER} value={indicatorProvider} /> @@ -126,7 +140,7 @@ export const ThreatMatchRowView = ({ contextId={contextId} data-test-subj="threat-match-row-indicator-reference" eventId={eventId} - field={'threat.indicator.event.reference'} + field={INDICATOR_REFERENCE} value={indicatorReference} /> From 0a6f6fd25952a9fad1a8ede1c81b2d7c2fc09fe4 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 11 Apr 2021 20:18:44 -0500 Subject: [PATCH 14/30] Fix example data for CTI row renderer * Adds missing Threat ECS types --- .../common/ecs/threat/index.ts | 4 +++ .../public/common/mock/mock_timeline_data.ts | 30 +++++++++++-------- .../examples/threat_match.tsx | 1 - 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/common/ecs/threat/index.ts b/x-pack/plugins/security_solution/common/ecs/threat/index.ts index 9273c01e13f697..19923a82dc846f 100644 --- a/x-pack/plugins/security_solution/common/ecs/threat/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/threat/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { EventEcs } from '../event'; + interface ThreatMatchEcs { atomic?: string[]; field?: string[]; @@ -13,6 +15,8 @@ interface ThreatMatchEcs { export interface ThreatIndicatorEcs { matched?: ThreatMatchEcs; + event?: EventEcs & { reference?: string[] }; + provider?: string[]; type?: string[]; } diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index d2a4619b26ad69..27f29112853e7a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -1090,21 +1090,27 @@ export const mockTimelineData: TimelineItem[] = [ }, { _id: '32', - data: [ - // TODO use more realistic data - { field: 'threat.indicator.matched.atomic', value: ['laptop.local'] }, - { field: 'threat.indicator.matched.field', value: ['host.name'] }, - { field: 'threat.indicator.matched.type', value: ['domain'] }, - { field: 'threat.indicator.event.dataset', value: ['threatintel.abuseurl'] }, - { - field: 'threat.indicator.event.reference', - value: ['https://urlhaus.abuse.ch/url/1055419/'], - }, - { field: 'threat.indicator.provider', value: ['indicator_provider'] }, - ], + data: [], ecs: { _id: 'BuBP4W0BOpWiDweSoYSg', timestamp: '2019-10-18T23:59:15.091Z', + // TODO use more realistic data + threat: { + indicator: [ + { + matched: { + atomic: ['laptop.local'], + field: ['host.name'], + type: ['domain'], + }, + event: { + dataset: ['threatintel.abuseurl'], + reference: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + provider: ['indicator_provider'], + }, + ], + }, }, }, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx index 81854563c4d14d..9d7e5d48315e37 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/threat_match.tsx @@ -16,7 +16,6 @@ const ThreatMatchExampleComponent: React.FC = () => ( {threatMatchRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[31].ecs, - flattenedData: mockTimelineData[31].data, timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} From f6f887c7ed0358291cd6cc006befbc41f5dd99d3 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 11 Apr 2021 20:44:18 -0500 Subject: [PATCH 15/30] Move CTI field constants to common folder In order to use these in both the row renderer and the server request, we need to move them to common/ --- .../body/renderers => common}/cti/constants.ts | 7 ++++--- .../detections/components/alerts_table/index.tsx | 4 ++-- .../components/timeline/body/renderers/cti/helpers.ts | 6 +++--- .../timeline/body/renderers/cti/threat_match_row.tsx | 2 +- .../timeline/factory/events/all/constants.ts | 11 ++--------- 5 files changed, 12 insertions(+), 18 deletions(-) rename x-pack/plugins/security_solution/{public/timelines/components/timeline/body/renderers => common}/cti/constants.ts (83%) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts similarity index 83% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts rename to x-pack/plugins/security_solution/common/cti/constants.ts index 2d81122519ae03..8be39d9bef938e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { INDICATOR_DESTINATION_PATH } from '../../../../../../../common/constants'; +import { INDICATOR_DESTINATION_PATH } from '../constants'; export const MATCHED_ATOMIC = 'matched.atomic'; export const MATCHED_FIELD = 'matched.field'; export const MATCHED_TYPE = 'matched.type'; -export const threatMatchSubFields = [MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE]; +export const INDICATOR_MATCH_SUBFIELDS = [MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE]; export const INDICATOR_MATCHED_ATOMIC = `${INDICATOR_DESTINATION_PATH}.${MATCHED_ATOMIC}`; export const INDICATOR_MATCHED_FIELD = `${INDICATOR_DESTINATION_PATH}.${MATCHED_FIELD}`; @@ -24,7 +24,8 @@ export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET} export const INDICATOR_REFERENCE = `${INDICATOR_DESTINATION_PATH}.${EVENT_REFERENCE}`; export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`; -export const requiredFields = [ +// fields used to populate the CTI row renderer +export const REQUIRED_INDICATOR_MATCH_FIELDS = [ INDICATOR_MATCHED_ATOMIC, INDICATOR_MATCHED_FIELD, INDICATOR_MATCHED_TYPE, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 88430f8165f018..7b84f5ff2e6d8c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -15,6 +15,7 @@ import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; +import { REQUIRED_INDICATOR_MATCH_FIELDS } from '../../../../common/cti/constants'; import { HeaderSection } from '../../../common/components/header_section'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { useKibana } from '../../../common/lib/kibana'; @@ -47,7 +48,6 @@ import { } from '../../../common/components/toasters'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; -import { requiredFields as requiredThreatMatchFields } from '../../../timelines/components/timeline/body/renderers/cti/constants'; import { buildTimeRangeFilter } from './helpers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; @@ -322,7 +322,7 @@ export const AlertsTableComponent: React.FC = ({ // TODO in the future, our alerts timeline fields should be derived from the // fields required by enabled row renderers and other functionality; for now we unconditionally // add the superset of fields. - queryFields: [...requiredFieldsForActions, ...requiredThreatMatchFields], + queryFields: [...requiredFieldsForActions, ...REQUIRED_INDICATOR_MATCH_FIELDS], title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts index 1c9cf5f5784449..08c61dd9922abc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts @@ -8,16 +8,16 @@ import { get, isEmpty } from 'lodash'; import { INDICATOR_DESTINATION_PATH } from '../../../../../../../common/constants'; +import { INDICATOR_MATCH_SUBFIELDS } from '../../../../../../../common/cti/constants'; import { Ecs } from '../../../../../../../common/ecs'; import { ThreatIndicatorEcs } from '../../../../../../../common/ecs/threat'; -import { threatMatchSubFields } from './constants'; const getIndicatorEcs = (data: Ecs): ThreatIndicatorEcs[] => get(data, INDICATOR_DESTINATION_PATH) ?? []; export const hasThreatMatchValue = (data: Ecs): boolean => getIndicatorEcs(data).some((indicator) => - threatMatchSubFields.some( - (threatMatchSubField) => !isEmpty(get(indicator, threatMatchSubField)) + INDICATOR_MATCH_SUBFIELDS.some( + (indicatorMatchSubField) => !isEmpty(get(indicator, indicatorMatchSubField)) ) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 90abe12263de07..222edc26374943 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -23,7 +23,7 @@ import { MATCHED_FIELD, MATCHED_TYPE, PROVIDER, -} from './constants'; +} from '../../../../../../../common/cti/constants'; export interface ThreatMatchRowProps { contextId: string; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts index 29b0df9e4bbf73..53af64081a707d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts @@ -5,14 +5,7 @@ * 2.0. */ -export const TIMELINE_CTI_FIELDS = [ - 'threat.indicator.event.dataset', - 'threat.indicator.event.reference', - 'threat.indicator.matched.atomic', - 'threat.indicator.matched.field', - 'threat.indicator.matched.type', - 'threat.indicator.provider', -]; +import { REQUIRED_INDICATOR_MATCH_FIELDS } from '../../../../../../common/cti/constants'; export const TIMELINE_EVENTS_FIELDS = [ '@timestamp', @@ -239,5 +232,5 @@ export const TIMELINE_EVENTS_FIELDS = [ 'zeek.ssl.established', 'zeek.ssl.resumed', 'zeek.ssl.version', - ...TIMELINE_CTI_FIELDS, + ...REQUIRED_INDICATOR_MATCH_FIELDS, ]; From 72b5b3fc3f4717dac38370f2c70567516de1e9eb Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 11 Apr 2021 20:56:37 -0500 Subject: [PATCH 16/30] Remove redundant CTI fields from client request These are currently hardcoded on the backend of the events/all query (via TIMELINE_EVENTS_FIELDS); declaring them on both ends is arguably confusing, and we're going with YAGNI for now. --- .../public/detections/components/alerts_table/index.tsx | 6 +----- .../timeline/factory/events/all/constants.ts | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 7b84f5ff2e6d8c..2890eb912b84c2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -15,7 +15,6 @@ import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; -import { REQUIRED_INDICATOR_MATCH_FIELDS } from '../../../../common/cti/constants'; import { HeaderSection } from '../../../common/components/header_section'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; import { useKibana } from '../../../common/lib/kibana'; @@ -319,10 +318,7 @@ export const AlertsTableComponent: React.FC = ({ id: timelineId, loadingText: i18n.LOADING_ALERTS, selectAll: false, - // TODO in the future, our alerts timeline fields should be derived from the - // fields required by enabled row renderers and other functionality; for now we unconditionally - // add the superset of fields. - queryFields: [...requiredFieldsForActions, ...REQUIRED_INDICATOR_MATCH_FIELDS], + queryFields: requiredFieldsForActions, title: '', }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts index 53af64081a707d..b704ae1798009e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts @@ -232,5 +232,8 @@ export const TIMELINE_EVENTS_FIELDS = [ 'zeek.ssl.established', 'zeek.ssl.resumed', 'zeek.ssl.version', + // TODO in the future, our alerts timeline fields should be derived from the + // fields required by enabled row renderers and other functionality; for now we unconditionally + // add the superset of fields. ...REQUIRED_INDICATOR_MATCH_FIELDS, ]; From f95f8e3e17a0e53e7cca98cc84153e664d108273 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 12 Apr 2021 13:38:24 -0500 Subject: [PATCH 17/30] Add missing graphQL type This was causing type errors as this enum exists both here and in common/, and I had only updated one of them. --- x-pack/plugins/security_solution/server/graphql/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 29d366e20c2998..a60a6dd6093d18 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -300,6 +300,7 @@ export enum RowRendererId { system_fim = 'system_fim', system_security_event = 'system_security_event', system_socket = 'system_socket', + threat_match = 'threat_match', zeek = 'zeek', } From 838e54874174ee436ba7f87f2bcfcf60c8e62b88 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 12 Apr 2021 13:42:53 -0500 Subject: [PATCH 18/30] Updates tests One is still failing due to an outdated test subject, but I expect this to change after an upcoming meeting so leaving it for now. --- .../threat_match_row.test.tsx.snap | 29 ++++++++++--------- .../threat_match_row_renderer.test.tsx.snap | 8 +++-- .../renderers/cti/threat_match_row.test.tsx | 8 ++++- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap index b6cafa841c6f9a..a985e3830b2742 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap @@ -1,9 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`threatMatchRowView matches the registered snapshot 1`] = ` +exports[`ThreatMatchRowView matches the registered snapshot 1`] = ` @@ -24,9 +25,9 @@ exports[`threatMatchRowView matches the registered snapshot 1`] = ` grow={false} > @@ -36,9 +37,9 @@ exports[`threatMatchRowView matches the registered snapshot 1`] = ` grow={false} > @@ -46,7 +47,7 @@ exports[`threatMatchRowView matches the registered snapshot 1`] = ` @@ -67,9 +68,9 @@ exports[`threatMatchRowView matches the registered snapshot 1`] = ` grow={false} > @@ -79,9 +80,9 @@ exports[`threatMatchRowView matches the registered snapshot 1`] = ` grow={false} > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap index b7d6c9d7272185..431c22f7892f65 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap @@ -5,8 +5,8 @@ exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1` - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx index ac2ef9f559736a..c5326eb0fbb163 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -12,12 +12,14 @@ import { TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ThreatMatchRowView } from './threat_match_row'; -describe('threatMatchRowView', () => { +describe('ThreatMatchRowView', () => { const mount = useMountAppended(); it('renders an indicator match row', () => { const wrapper = shallow( { it('matches the registered snapshot', () => { const wrapper = shallow( { const wrapper = mount( Date: Wed, 14 Apr 2021 14:25:44 -0500 Subject: [PATCH 19/30] Split ThreatMatchRow into subcomponents One for displaying match details, and another for indicator details The indicator details will be sparse, so there's going to be some conditional rendering in there. --- .../body/renderers/cti/indicator_details.tsx | 83 +++++++++++++ .../body/renderers/cti/match_details.tsx | 55 +++++++++ .../body/renderers/cti/threat_match_row.tsx | 113 ++++-------------- .../body/renderers/cti/threat_match_rows.tsx | 25 ++-- 4 files changed, 178 insertions(+), 98 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx new file mode 100644 index 00000000000000..7184ce649b80a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + INDICATOR_DATASET, + INDICATOR_MATCHED_TYPE, + INDICATOR_PROVIDER, + INDICATOR_REFERENCE, +} from '../../../../../../../common/cti/constants'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; + +interface IndicatorDetailsProps { + contextId: string; + eventId: string; + indicatorDataset: string | undefined; + indicatorProvider: string | undefined; + indicatorReference: string | undefined; + indicatorType: string | undefined; +} + +export const IndicatorDetails: React.FC = ({ + contextId, + eventId, + indicatorDataset, + indicatorProvider, + indicatorReference, + indicatorType, +}) => ( + + + + + {''} + + + + {'via'} + + + + {':'} + + + + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx new file mode 100644 index 00000000000000..6962cd0c55ee14 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { INDICATOR_MATCHED_FIELD } from '../../../../../../../common/cti/constants'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; + +interface MatchDetailsProps { + contextId: string; + eventId: string; + sourceField: string; + sourceValue: string; +} + +export const MatchDetails: React.FC = ({ + contextId, + eventId, + sourceField, + sourceValue, +}) => ( + + {'match found on'} + + + + {'whose value was'} + + + + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 222edc26374943..25f179be80ead4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -6,32 +6,27 @@ */ import { get } from 'lodash'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { Fields } from '../../../../../../../common/search_strategy'; -import { DraggableBadge } from '../../../../../../common/components/draggables'; import { EVENT_DATASET, EVENT_REFERENCE, - INDICATOR_DATASET, - INDICATOR_MATCHED_FIELD, - INDICATOR_MATCHED_TYPE, - INDICATOR_PROVIDER, - INDICATOR_REFERENCE, MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE, PROVIDER, } from '../../../../../../../common/cti/constants'; +import { MatchDetails } from './match_details'; +import { IndicatorDetails } from './indicator_details'; export interface ThreatMatchRowProps { contextId: string; eventId: string; - indicatorDataset: string; - indicatorProvider: string; - indicatorReference: string; - indicatorType: string; + indicatorDataset: string | undefined; + indicatorProvider: string | undefined; + indicatorReference: string | undefined; + indicatorType: string | undefined; sourceField: string; sourceValue: string; } @@ -48,10 +43,10 @@ export const ThreatMatchRow = ({ const props = { contextId: `threat-match-row-${timelineId}-${eventId}`, eventId, - indicatorDataset: get(data, EVENT_DATASET)[0] as string, - indicatorReference: get(data, EVENT_REFERENCE)[0] as string, - indicatorProvider: get(data, PROVIDER)[0] as string, - indicatorType: get(data, MATCHED_TYPE)[0] as string, + indicatorDataset: get(data, EVENT_DATASET)[0] as string | undefined, + indicatorReference: get(data, EVENT_REFERENCE)[0] as string | undefined, + indicatorProvider: get(data, PROVIDER)[0] as string | undefined, + indicatorType: get(data, MATCHED_TYPE)[0] as string | undefined, sourceField: get(data, MATCHED_FIELD)[0] as string, sourceValue: get(data, MATCHED_ATOMIC)[0] as string, }; @@ -71,80 +66,20 @@ export const ThreatMatchRowView = ({ }: ThreatMatchRowProps) => { return ( <> - - - - - {'indicator matched on'} - - - - {', whose value was'} - - - - - - - - - {'via'} - - - - {':'} - - - - + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index c6198aa6e39e3c..6ddf9a1fbd8a5f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -7,27 +7,34 @@ import { get } from 'lodash'; import React from 'react'; +import styled from 'styled-components'; import { Fields } from '../../../../../../../common/search_strategy'; import { ID_FIELD_NAME } from '../../../../../../common/components/event_details/event_id'; import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { ThreatMatchRow } from './threat_match_row'; +const SpacedContainer = styled.div` + margin: ${({ theme }) => theme.eui.paddingSizes.s} 0; +`; + export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) => { const indicators = get(data, 'threat.indicator') as Fields[]; const eventId = get(data, ID_FIELD_NAME); return ( - {indicators.map((indicator, index) => ( - - ))} + + {indicators.map((indicator, index) => ( + + ))} + ); }; From d625b2c64d2a33fb0e100e02edb5b248310ea7b5 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 14 Apr 2021 17:48:48 -0500 Subject: [PATCH 20/30] Make CTI row renderer look nice * Adds translations for copy * Fixes most of our layout woes with more flexbox! * Conditional rendering of indicator details based on data * tests --- .../threat_match_row.test.tsx.snap | 113 +++---------- .../threat_match_row_renderer.test.tsx.snap | 40 ++--- .../timeline/body/renderers/cti/helpers.ts | 5 + .../body/renderers/cti/indicator_details.tsx | 111 ++++++++----- .../body/renderers/cti/match_details.tsx | 15 +- .../renderers/cti/threat_match_row.test.tsx | 157 +++++++++++++++--- .../body/renderers/cti/threat_match_row.tsx | 42 +++-- 7 files changed, 297 insertions(+), 186 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap index a985e3830b2742..5e86ba25e4ba8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap @@ -1,92 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ThreatMatchRowView matches the registered snapshot 1`] = ` - - + - - - - indicator matched on - - - - , whose value was - - - - - + + - - - - via - - - - : - - - - - + + + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap index 431c22f7892f65..d69bab9a75d614 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap @@ -5,26 +5,28 @@ exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1` - + + eventId="1" + key="threat-match-row-1-0" + timelineId="test" + /> + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts index 08c61dd9922abc..6a341e34ee1905 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts @@ -6,6 +6,7 @@ */ import { get, isEmpty } from 'lodash'; +import styled from 'styled-components'; import { INDICATOR_DESTINATION_PATH } from '../../../../../../../common/constants'; import { INDICATOR_MATCH_SUBFIELDS } from '../../../../../../../common/cti/constants'; @@ -21,3 +22,7 @@ export const hasThreatMatchValue = (data: Ecs): boolean => (indicatorMatchSubField) => !isEmpty(get(indicator, indicatorMatchSubField)) ) ); + +export const HorizontalSpacer = styled.div` + margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; +`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx index 7184ce649b80a8..a080290abd23a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { INDICATOR_DATASET, @@ -15,6 +16,7 @@ import { INDICATOR_REFERENCE, } from '../../../../../../../common/cti/constants'; import { DraggableBadge } from '../../../../../../common/components/draggables'; +import { HorizontalSpacer } from './helpers'; interface IndicatorDetailsProps { contextId: string; @@ -39,45 +41,76 @@ export const IndicatorDetails: React.FC = ({ direction="row" justifyContent="center" gutterSize="none" + wrap > - - - - {''} - - - - {'via'} - - - - {':'} - - - + {indicatorType && ( + + + + )} + {indicatorDataset && ( + <> + + + + + + + + + + )} + {indicatorProvider && ( + <> + + + + + + + + + + )} + {indicatorReference && ( + <> + + {':'} + + + + + + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx index 6962cd0c55ee14..2195421301d318 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/match_details.tsx @@ -7,9 +7,11 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { INDICATOR_MATCHED_FIELD } from '../../../../../../../common/cti/constants'; import { DraggableBadge } from '../../../../../../common/components/draggables'; +import { HorizontalSpacer } from './helpers'; interface MatchDetailsProps { contextId: string; @@ -25,13 +27,13 @@ export const MatchDetails: React.FC = ({ sourceValue, }) => ( - {'match found on'} = ({ value={sourceField} /> - {'whose value was'} + + + + + { const mount = useMountAppended(); @@ -49,28 +49,139 @@ describe('ThreatMatchRowView', () => { expect(wrapper).toMatchSnapshot(); }); - it('renders draggable fields', () => { - const wrapper = mount( - - - - ); + describe('field rendering', () => { + let baseProps: ThreatMatchRowProps; + const render = (props: ThreatMatchRowProps) => + mount( + + + + ); - const sourceValueDraggable = wrapper.find('[data-test-subj="threat-match-row-source-value"]'); - expect(sourceValueDraggable.props()).toEqual( - expect.objectContaining({ - field: 'host.name', - value: 'http://elastic.co', - }) - ); + beforeEach(() => { + baseProps = { + contextId: 'contextId', + eventId: 'eventId', + indicatorDataset: 'dataset', + indicatorProvider: 'provider', + indicatorReference: 'http://example.com', + indicatorType: 'domain', + sourceField: 'host.name', + sourceValue: 'http://elastic.co', + }; + }); + + it('renders the match field', () => { + const wrapper = render(baseProps); + const matchField = wrapper.find('[data-test-subj="threat-match-details-source-field"]'); + expect(matchField.props()).toEqual( + expect.objectContaining({ + value: 'host.name', + }) + ); + }); + + it('renders the match value', () => { + const wrapper = render(baseProps); + const matchValue = wrapper.find('[data-test-subj="threat-match-details-source-value"]'); + expect(matchValue.props()).toEqual( + expect.objectContaining({ + field: 'host.name', + value: 'http://elastic.co', + }) + ); + }); + + it('renders the indicator type, if present', () => { + const wrapper = render(baseProps); + const indicatorType = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-type"]' + ); + expect(indicatorType.props()).toEqual( + expect.objectContaining({ + value: 'domain', + }) + ); + }); + + it('does not render the indicator type, if absent', () => { + const wrapper = render({ + ...baseProps, + indicatorType: undefined, + }); + const indicatorType = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-type"]' + ); + expect(indicatorType.exists()).toBeFalsy(); + }); + + it('renders the indicator dataset, if present', () => { + const wrapper = render(baseProps); + const indicatorDataset = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-dataset"]' + ); + expect(indicatorDataset.props()).toEqual( + expect.objectContaining({ + value: 'dataset', + }) + ); + }); + + it('does not render the indicator dataset, if absent', () => { + const wrapper = render({ + ...baseProps, + indicatorDataset: undefined, + }); + const indicatorDataset = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-dataset"]' + ); + expect(indicatorDataset.exists()).toBeFalsy(); + }); + + it('renders the indicator provider, if present', () => { + const wrapper = render(baseProps); + const indicatorProvider = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-provider"]' + ); + expect(indicatorProvider.props()).toEqual( + expect.objectContaining({ + value: 'provider', + }) + ); + }); + + it('does not render the indicator provider, if absent', () => { + const wrapper = render({ + ...baseProps, + indicatorProvider: undefined, + }); + const indicatorProvider = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-provider"]' + ); + expect(indicatorProvider.exists()).toBeFalsy(); + }); + + it('renders the indicator reference, if present', () => { + const wrapper = render(baseProps); + const indicatorReference = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-reference"]' + ); + expect(indicatorReference.props()).toEqual( + expect.objectContaining({ + value: 'http://example.com', + }) + ); + }); + + it('does not render the indicator reference, if absent', () => { + const wrapper = render({ + ...baseProps, + indicatorReference: undefined, + }); + const indicatorReference = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-reference"]' + ); + expect(indicatorReference.exists()).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 25f179be80ead4..694baf6ae1d879 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -7,6 +7,7 @@ import { get } from 'lodash'; import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Fields } from '../../../../../../../common/search_strategy'; import { @@ -65,21 +66,30 @@ export const ThreatMatchRowView = ({ sourceValue, }: ThreatMatchRowProps) => { return ( - <> - - - + + + + + + + + ); }; From d14f795529ba0b0099f891691f75ef5bed5a8d51 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 14 Apr 2021 19:26:00 -0500 Subject: [PATCH 21/30] Make indicator reference field an external link Leverages the existing FormattedFieldValue component, with one minor tweak to add this field to the URL allowlist. --- .../timeline/body/renderers/cti/indicator_details.tsx | 5 +++-- .../timeline/body/renderers/formatted_field.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx index a080290abd23a0..11846632f740ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx @@ -16,6 +16,7 @@ import { INDICATOR_REFERENCE, } from '../../../../../../../common/cti/constants'; import { DraggableBadge } from '../../../../../../common/components/draggables'; +import { FormattedFieldValue } from '../formatted_field'; import { HorizontalSpacer } from './helpers'; interface IndicatorDetailsProps { @@ -102,11 +103,11 @@ export const IndicatorDetails: React.FC = ({ {':'} - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index e227c87b99870e..12effcd3fa81fb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -9,6 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { isNumber, isEmpty } from 'lodash/fp'; import React from 'react'; +import { INDICATOR_REFERENCE } from '../../../../../../common/cti/constants'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { Bytes, BYTES_FORMAT } from './bytes'; import { Duration, EVENT_DURATION_FIELD_NAME } from '../../../duration'; @@ -116,7 +117,12 @@ const FormattedFieldValueComponent: React.FC<{ ); } else if ( - [RULE_REFERENCE_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME].includes(fieldName) + [ + RULE_REFERENCE_FIELD_NAME, + REFERENCE_URL_FIELD_NAME, + EVENT_URL_FIELD_NAME, + INDICATOR_REFERENCE, + ].includes(fieldName) ) { return renderUrl({ contextId, eventId, fieldName, linkValue, truncate, value }); } else if (columnNamesNotDraggable.includes(fieldName)) { From 5fbcf64863ff07cb3934621d04b733640c7cfddb Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 14 Apr 2021 19:26:49 -0500 Subject: [PATCH 22/30] Back to consistent horizontal spacing, here The draggable badges are a little odd in that their full box isn't indicated until hover, making the visual weight a little off. --- .../timelines/components/timeline/body/renderers/cti/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts index 6a341e34ee1905..84dcef327736bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/helpers.ts @@ -24,5 +24,5 @@ export const hasThreatMatchValue = (data: Ecs): boolean => ); export const HorizontalSpacer = styled.div` - margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; + margin: 0 ${({ theme }) => theme.eui.paddingSizes.xs}; `; From d7cce752a83a12c53bd7f0813dda0d5802ea48a0 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 14 Apr 2021 19:39:32 -0500 Subject: [PATCH 23/30] Add hr as a visual separator between each match "row" of the row renderer --- .../threat_match_row_renderer.test.tsx.snap | 1 - .../body/renderers/cti/threat_match_rows.tsx | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap index d69bab9a75d614..84a3f739401a8f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap @@ -23,7 +23,6 @@ exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1` } } eventId="1" - key="threat-match-row-1-0" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 6ddf9a1fbd8a5f..62e82cbc2c9693 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -5,8 +5,9 @@ * 2.0. */ +import { EuiHorizontalRule } from '@elastic/eui'; import { get } from 'lodash'; -import React from 'react'; +import React, { Fragment } from 'react'; import styled from 'styled-components'; import { Fields } from '../../../../../../../common/search_strategy'; @@ -26,13 +27,11 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) {indicators.map((indicator, index) => ( - + // index should be replaced with matched.id when it is available + + + {index < indicators.length - 1 && } + ))} From 5a04c28476e87cf6dc302fc4d70b302d900df084 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 14 Apr 2021 19:56:23 -0500 Subject: [PATCH 24/30] Fix tests broken due to addition of a new row renderer These tests are all implicitly testing the list of row renderers. --- .../eql_tab_content/__snapshots__/index.test.tsx.snap | 5 +++++ .../pinned_tab_content/__snapshots__/index.test.tsx.snap | 5 +++++ .../query_tab_content/__snapshots__/index.test.tsx.snap | 5 +++++ .../public/timelines/containers/index.test.tsx | 4 ++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap index 7d237ecaf92df1..9ec1fa70712779 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap @@ -143,6 +143,11 @@ In other use cases the message field can be used to concatenate different values renderCellValue={[Function]} rowRenderers={ Array [ + Object { + "id": "threat_match", + "isInstance": [Function], + "renderRow": [Function], + }, Object { "id": "auditd", "isInstance": [Function], diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/__snapshots__/index.test.tsx.snap index ef73ba9f24db33..ce59d191a472d6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/__snapshots__/index.test.tsx.snap @@ -138,6 +138,11 @@ In other use cases the message field can be used to concatenate different values renderCellValue={[Function]} rowRenderers={ Array [ + Object { + "id": "threat_match", + "isInstance": [Function], + "renderRow": [Function], + }, Object { "id": "auditd", "isInstance": [Function], diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index 46c85f634ff6b7..f6ff6b50221b70 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -279,6 +279,11 @@ In other use cases the message field can be used to concatenate different values renderCellValue={[Function]} rowRenderers={ Array [ + Object { + "id": "threat_match", + "isInstance": [Function], + "renderRow": [Function], + }, Object { "id": "auditd", "isInstance": [Function], diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index b24a50a516325c..dc808961729207 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -159,7 +159,7 @@ describe('useTimelineEvents', () => { loadPage: result.current[1].loadPage, pageInfo: result.current[1].pageInfo, refetch: result.current[1].refetch, - totalCount: 31, + totalCount: 32, updatedAt: result.current[1].updatedAt, }, ]); @@ -202,7 +202,7 @@ describe('useTimelineEvents', () => { loadPage: result.current[1].loadPage, pageInfo: result.current[1].pageInfo, refetch: result.current[1].refetch, - totalCount: 31, + totalCount: 32, updatedAt: result.current[1].updatedAt, }, ]); From d8bf839ffcf717a01c97b2d7f65362722cd1cb73 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 14 Apr 2021 20:19:47 -0500 Subject: [PATCH 25/30] Full-width hr At certain container widths, a half-width hr is not sufficient. --- .../timeline/body/renderers/cti/threat_match_rows.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 62e82cbc2c9693..9d334256cee79a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -30,7 +30,7 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) // index should be replaced with matched.id when it is available - {index < indicators.length - 1 && } + {index < indicators.length - 1 && } ))} From 2f7433f02274225583b9bdff70df8b8262b80762 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 15 Apr 2021 13:29:57 -0500 Subject: [PATCH 26/30] More descriptive constant Obviates the need for the accompanying comments. --- x-pack/plugins/security_solution/common/cti/constants.ts | 3 +-- .../timeline/factory/events/all/constants.ts | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 8be39d9bef938e..cdd4a564f3d73c 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -24,8 +24,7 @@ export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET} export const INDICATOR_REFERENCE = `${INDICATOR_DESTINATION_PATH}.${EVENT_REFERENCE}`; export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`; -// fields used to populate the CTI row renderer -export const REQUIRED_INDICATOR_MATCH_FIELDS = [ +export const CTI_ROW_RENDERER_FIELDS = [ INDICATOR_MATCHED_ATOMIC, INDICATOR_MATCHED_FIELD, INDICATOR_MATCHED_TYPE, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts index b704ae1798009e..38188a1616bfc3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { REQUIRED_INDICATOR_MATCH_FIELDS } from '../../../../../../common/cti/constants'; +import { CTI_ROW_RENDERER_FIELDS } from '../../../../../../common/cti/constants'; export const TIMELINE_EVENTS_FIELDS = [ '@timestamp', @@ -232,8 +232,5 @@ export const TIMELINE_EVENTS_FIELDS = [ 'zeek.ssl.established', 'zeek.ssl.resumed', 'zeek.ssl.version', - // TODO in the future, our alerts timeline fields should be derived from the - // fields required by enabled row renderers and other functionality; for now we unconditionally - // add the superset of fields. - ...REQUIRED_INDICATOR_MATCH_FIELDS, + ...CTI_ROW_RENDERER_FIELDS, ]; From d49d69234d0e06e81b9b474031c000818f53613a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 15 Apr 2021 13:40:58 -0500 Subject: [PATCH 27/30] More realistic data Also ensures less traffic to urlhaus ;) --- .../public/common/mock/mock_timeline_data.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index 27f29112853e7a..6a3c6468f43d59 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -1094,18 +1094,17 @@ export const mockTimelineData: TimelineItem[] = [ ecs: { _id: 'BuBP4W0BOpWiDweSoYSg', timestamp: '2019-10-18T23:59:15.091Z', - // TODO use more realistic data threat: { indicator: [ { matched: { - atomic: ['laptop.local'], - field: ['host.name'], - type: ['domain'], + atomic: ['192.168.1.1'], + field: ['source.ip'], + type: ['ip'], }, event: { - dataset: ['threatintel.abuseurl'], - reference: ['https://urlhaus.abuse.ch/url/1055419/'], + dataset: ['threatintel.example_dataset'], + reference: ['https://example.com'], }, provider: ['indicator_provider'], }, From 542052babca9b0a7dcf2720fba3605da581f5752 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 15 Apr 2021 13:58:51 -0500 Subject: [PATCH 28/30] Remove useless comment --- .../components/timeline/body/renderers/cti/threat_match_rows.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 9d334256cee79a..01bb97afe0ac13 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -27,7 +27,6 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) {indicators.map((indicator, index) => ( - // index should be replaced with matched.id when it is available {index < indicators.length - 1 && } From 9295afc399b0f764dbaf08592816bab679a6fc87 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 15 Apr 2021 14:05:50 -0500 Subject: [PATCH 29/30] Add threat_match row renderer type to GQL client Gennin' beanz --- .../security_solution/public/graphql/introspection.json | 6 ++++++ x-pack/plugins/security_solution/public/graphql/types.ts | 1 + .../security_solution/server/graphql/timeline/schema.gql.ts | 1 + 3 files changed, 8 insertions(+) diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 0a41ca05b8753b..752173ded5163e 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -1699,6 +1699,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "threat_match", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "zeek", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 8ffd2995d0d978..a41111c3e123ab 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -298,6 +298,7 @@ export enum RowRendererId { system_fim = 'system_fim', system_security_event = 'system_security_event', system_socket = 'system_socket', + threat_match = 'threat_match', zeek = 'zeek', } diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 05a824e3630bd4..98e7103e612247 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -171,6 +171,7 @@ export const timelineSchema = gql` system_fim system_security_event system_socket + threat_match zeek } From b115dfea5a0ddbcefab434ab527ba609a71185a9 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 15 Apr 2021 14:12:14 -0500 Subject: [PATCH 30/30] Ensure contextId is unique for each CTI subrow We need to add the row index to our contextId to ensure that our draggables work correctly for multiple rows, since each row will necessarily have the same eventId and timelineId. --- .../threat_match_row_renderer.test.tsx.snap | 2 +- .../body/renderers/cti/threat_match_row.tsx | 6 +++--- .../body/renderers/cti/threat_match_rows.tsx | 15 +++++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap index 84a3f739401a8f..6e6dbddc6d9a03 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row_renderer.test.tsx.snap @@ -7,6 +7,7 @@ exports[`threatMatchRowRenderer #renderRow renders correctly against snapshot 1` > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 694baf6ae1d879..ba5b0127df526b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -33,16 +33,16 @@ export interface ThreatMatchRowProps { } export const ThreatMatchRow = ({ + contextId, data, eventId, - timelineId, }: { + contextId: string; data: Fields; eventId: string; - timelineId: string; }) => { const props = { - contextId: `threat-match-row-${timelineId}-${eventId}`, + contextId, eventId, indicatorDataset: get(data, EVENT_DATASET)[0] as string | undefined, indicatorReference: get(data, EVENT_REFERENCE)[0] as string | undefined, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index 01bb97afe0ac13..cc34f9e63b5e2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -26,12 +26,15 @@ export const ThreatMatchRows: RowRenderer['renderRow'] = ({ data, timelineId }) return ( - {indicators.map((indicator, index) => ( - - - {index < indicators.length - 1 && } - - ))} + {indicators.map((indicator, index) => { + const contextId = `threat-match-row-${timelineId}-${eventId}-${index}`; + return ( + + + {index < indicators.length - 1 && } + + ); + })} );