From 2b68cc33222a7fa54956785638ddb7700971f5d9 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 17 Jun 2021 23:04:34 -0500 Subject: [PATCH 01/38] Add pure fn and consuming hook to fetch event enrichment It's not being invoked yet, but I've added a placeholder where it's going. --- .../containers/events/event_enrichment/api.ts | 40 +++++++++++++++++++ .../events/event_enrichment/index.ts | 8 ++++ .../event_enrichment/use_event_enrichment.ts | 15 +++++++ .../side_panel/event_details/index.tsx | 2 + 4 files changed, 65 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/api.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/use_event_enrichment.ts diff --git a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/api.ts b/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/api.ts new file mode 100644 index 0000000000000..245cea6a1e21a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/api.ts @@ -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 { Observable } from 'rxjs'; + +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { + CtiEventEnrichmentRequestOptions, + CtiEventEnrichmentStrategyResponse, + CtiQueries, +} from '../../../../../common/search_strategy/security_solution/cti'; + +export const getEventEnrichment = ({ + data, + defaultIndex, + eventFields, + filterQuery, + timerange, + signal, +}: CtiEventEnrichmentRequestOptions & { + data: DataPublicPluginStart; + signal: AbortSignal; +}): Observable => + data.search.search( + { + defaultIndex, + eventFields, + factoryQueryType: CtiQueries.eventEnrichment, + filterQuery, + timerange, + }, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: signal, + } + ); diff --git a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts new file mode 100644 index 0000000000000..4ce16a2b0e542 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './use_event_enrichment'; diff --git a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/use_event_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/use_event_enrichment.ts new file mode 100644 index 0000000000000..ddc9c27f68c7a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/use_event_enrichment.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 { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; + +import { getEventEnrichment } from './api'; + +// TODO define filtered version of getEventEnrichment that excludes incomplete responses +const getEventEnrichmentOptionalSignal = withOptionalSignal(getEventEnrichment); + +export const useEventEnrichment = () => useObservable(getEventEnrichmentOptionalSignal); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 76341055f28ef..e1c002611d108 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -73,6 +73,8 @@ const EventDetailsPanelComponent: React.FC = ({ skip: !expandedEvent.eventId, }); + // TODO get data here + const [isHostIsolationPanelOpen, setIsHostIsolationPanel] = useState(false); const [isolateAction, setIsolateAction] = useState('isolateHost'); From a85aa966166e8e367fdc33d7058a605e69eabe03 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 20 Jun 2021 20:14:45 -0500 Subject: [PATCH 02/38] Move existing enrichment tests to new spec file This is a rough copy/paste, I'll clean up as I flesh out the new tests. --- .../detection_alerts/cti_enrichments.spec.ts | 207 ++++++++++++++++++ .../indicator_match_rule.spec.ts | 172 +-------------- 2 files changed, 208 insertions(+), 171 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts new file mode 100644 index 0000000000000..7556b5f4342aa --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -0,0 +1,207 @@ +/* + * 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 { indexPatterns, newThreatIndicatorRule } from '../../objects/rule'; +import { cleanKibana, reload } from '../../tasks/common'; +import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { + JSON_LINES, + TABLE_CELL, + TABLE_ROWS, + THREAT_CONTENT, + THREAT_DETAILS_VIEW, + THREAT_INTEL_TAB, + THREAT_SUMMARY_VIEW, + TITLE, +} from '../../screens/alerts_details'; +import { TIMELINE_FIELD } from '../../screens/rule_details'; +import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; +import { + expandFirstAlert, + goToManageAlertsDetectionRules, + investigateFirstAlertInTimeline, + waitForAlertsIndexToBeCreated, + waitForAlertsPanelToBeLoaded, +} from '../../tasks/alerts'; +import { createCustomRuleActivated, createCustomIndicatorRule } from '../../tasks/api_calls/rules'; +import { + openJsonView, + openThreatIndicatorDetails, + scrollJsonViewToBottom, +} from '../../tasks/alerts_details'; + +import { DETECTIONS_URL } from '../../urls/navigation'; +import { addsFieldsToTimeline } from '../../tasks/rule_details'; + +// describe('CTI enrichments', () => { +// beforeEach(() => { +// cleanKibana(); +// esArchiverLoad('unmapped_fields'); +// loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); +// waitForAlertsPanelToBeLoaded(); +// waitForAlertsIndexToBeCreated(); +// // createCustomRuleActivated(unmappedRule); +// loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); +// waitForAlertsPanelToBeLoaded(); +// expandFirstAlert(); +// }); + +// it('displays enrichments from indicator match rules and from investigation time'); +// it('displays investigation time enrichments from a longer time range'); +// }); + +describe('CTI Enrichment', () => { + const fieldSearch = 'threat.indicator.matched'; + const fields = [ + 'threat.indicator.matched.atomic', + 'threat.indicator.matched.type', + 'threat.indicator.matched.field', + ]; + const expectedFieldsText = [ + newThreatIndicatorRule.atomic, + newThreatIndicatorRule.type, + newThreatIndicatorRule.indicatorMappingField, + ]; + + const expectedEnrichment = [ + { line: 4, text: ' "threat": {' }, + { + line: 3, + text: + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + }, + { line: 2, text: ' }' }, + ]; + + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('suspicious_source_event'); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('suspicious_source_event'); + }); + + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + goToManageAlertsDetectionRules(); + goToRuleDetails(); + }); + + it('Displays matches on the timeline', () => { + addsFieldsToTimeline(fieldSearch, fields); + + fields.forEach((field, index) => { + cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFieldsText[index]); + }); + }); + + it('Displays enrichment on the JSON view', () => { + expandFirstAlert(); + openJsonView(); + scrollJsonViewToBottom(); + + cy.get(JSON_LINES).then((elements) => { + const length = elements.length; + expectedEnrichment.forEach((enrichment) => { + cy.wrap(elements) + .eq(length - enrichment.line) + .should('have.text', enrichment.text); + }); + }); + }); + + it('Displays threat summary data on alerts details', () => { + const expectedThreatSummary = [ + { field: 'matched.field', value: 'myhash.mysha256' }, + { field: 'matched.type', value: 'file' }, + { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, + ]; + + expandFirstAlert(); + + cy.get(THREAT_SUMMARY_VIEW).within(() => { + cy.get(TABLE_ROWS).should('have.length', expectedThreatSummary.length); + expectedThreatSummary.forEach((row, index) => { + cy.get(TABLE_ROWS) + .eq(index) + .within(() => { + cy.get(TITLE).should('have.text', row.field); + cy.get(THREAT_CONTENT).should('have.text', row.value); + }); + }); + }); + }); + + it('Displays threat indicator data on the threat intel tab', () => { + const expectedThreatIndicatorData = [ + { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, + { field: 'file.size', value: '80280' }, + { field: 'file.type', value: 'elf' }, + { + field: 'file.hash.sha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }, + { + field: 'file.hash.tlsh', + value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', + }, + { + field: 'file.hash.ssdeep', + value: '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', + }, + { field: 'file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, + { field: 'type', value: 'file' }, + { + field: 'event.reference', + value: + 'https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/(opens in a new tab or window)', + }, + { field: 'event.ingested', value: '2021-03-10T14:51:09.809069Z' }, + { field: 'event.created', value: '2021-03-10T14:51:07.663Z' }, + { field: 'event.kind', value: 'enrichment' }, + { field: 'event.module', value: 'threatintel' }, + { field: 'event.category', value: 'threat' }, + { field: 'event.type', value: 'indicator' }, + { field: 'event.dataset', value: 'threatintel.abusemalware' }, + { + field: 'matched.atomic', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }, + { field: 'matched.field', value: 'myhash.mysha256' }, + { + field: 'matched.id', + value: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', + }, + { field: 'matched.index', value: 'filebeat-7.12.0-2021.03.10-000001' }, + { field: 'matched.type', value: 'file' }, + ]; + + expandFirstAlert(); + openThreatIndicatorDetails(); + + cy.get(THREAT_INTEL_TAB).should('have.text', 'Threat Intel (1)'); + cy.get(THREAT_DETAILS_VIEW).within(() => { + cy.get(TABLE_ROWS).should('have.length', expectedThreatIndicatorData.length); + expectedThreatIndicatorData.forEach((row, index) => { + cy.get(TABLE_ROWS) + .eq(index) + .within(() => { + cy.get(TABLE_CELL).eq(0).should('have.text', row.field); + cy.get(TABLE_CELL).eq(1).should('have.text', row.value); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index bc8cf0137fa83..27430ce9c8444 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -16,16 +16,6 @@ import { ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; -import { - JSON_LINES, - TABLE_CELL, - TABLE_ROWS, - THREAT_CONTENT, - THREAT_DETAILS_VIEW, - THREAT_INTEL_TAB, - THREAT_SUMMARY_VIEW, - TITLE, -} from '../../screens/alerts_details'; import { CUSTOM_RULES_BTN, RISK_SCORE, @@ -60,23 +50,15 @@ import { SCHEDULE_DETAILS, SEVERITY_DETAILS, TAGS_DETAILS, - TIMELINE_FIELD, TIMELINE_TEMPLATE_DETAILS, } from '../../screens/rule_details'; import { INDICATOR_MATCH_ROW_RENDER, PROVIDER_BADGE } from '../../screens/timeline'; - import { - expandFirstAlert, goToManageAlertsDetectionRules, investigateFirstAlertInTimeline, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; -import { - openJsonView, - openThreatIndicatorDetails, - scrollJsonViewToBottom, -} from '../../tasks/alerts_details'; import { changeRowsPerPageTo100, duplicateFirstRule, @@ -121,7 +103,7 @@ import { import { goBackToRuleDetails, waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { addsFieldsToTimeline, goBackToAllRulesTable } from '../../tasks/rule_details'; +import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -532,158 +514,6 @@ describe('indicator match', () => { }); }); - describe('Enrichment', () => { - const fieldSearch = 'threat.indicator.matched'; - const fields = [ - 'threat.indicator.matched.atomic', - 'threat.indicator.matched.type', - 'threat.indicator.matched.field', - ]; - const expectedFieldsText = [ - newThreatIndicatorRule.atomic, - newThreatIndicatorRule.type, - newThreatIndicatorRule.indicatorMappingField, - ]; - - const expectedEnrichment = [ - { line: 4, text: ' "threat": {' }, - { - line: 3, - text: - ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', - }, - { line: 2, text: ' }' }, - ]; - - before(() => { - cleanKibana(); - esArchiverLoad('threat_indicator'); - esArchiverLoad('suspicious_source_event'); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - goToManageAlertsDetectionRules(); - createCustomIndicatorRule(newThreatIndicatorRule); - reload(); - }); - - after(() => { - esArchiverUnload('threat_indicator'); - esArchiverUnload('suspicious_source_event'); - }); - - beforeEach(() => { - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - goToManageAlertsDetectionRules(); - goToRuleDetails(); - }); - - it('Displays matches on the timeline', () => { - addsFieldsToTimeline(fieldSearch, fields); - - fields.forEach((field, index) => { - cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFieldsText[index]); - }); - }); - - it('Displays enrichment on the JSON view', () => { - expandFirstAlert(); - openJsonView(); - scrollJsonViewToBottom(); - - cy.get(JSON_LINES).then((elements) => { - const length = elements.length; - expectedEnrichment.forEach((enrichment) => { - cy.wrap(elements) - .eq(length - enrichment.line) - .should('have.text', enrichment.text); - }); - }); - }); - - it('Displays threat summary data on alerts details', () => { - const expectedThreatSummary = [ - { field: 'matched.field', value: 'myhash.mysha256' }, - { field: 'matched.type', value: 'file' }, - { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, - ]; - - expandFirstAlert(); - - cy.get(THREAT_SUMMARY_VIEW).within(() => { - cy.get(TABLE_ROWS).should('have.length', expectedThreatSummary.length); - expectedThreatSummary.forEach((row, index) => { - cy.get(TABLE_ROWS) - .eq(index) - .within(() => { - cy.get(TITLE).should('have.text', row.field); - cy.get(THREAT_CONTENT).should('have.text', row.value); - }); - }); - }); - }); - - it('Displays threat indicator data on the threat intel tab', () => { - const expectedThreatIndicatorData = [ - { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, - { field: 'file.size', value: '80280' }, - { field: 'file.type', value: 'elf' }, - { - field: 'file.hash.sha256', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', - }, - { - field: 'file.hash.tlsh', - value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', - }, - { - field: 'file.hash.ssdeep', - value: - '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', - }, - { field: 'file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, - { field: 'type', value: 'file' }, - { - field: 'event.reference', - value: - 'https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/(opens in a new tab or window)', - }, - { field: 'event.ingested', value: '2021-03-10T14:51:09.809069Z' }, - { field: 'event.created', value: '2021-03-10T14:51:07.663Z' }, - { field: 'event.kind', value: 'enrichment' }, - { field: 'event.module', value: 'threatintel' }, - { field: 'event.category', value: 'threat' }, - { field: 'event.type', value: 'indicator' }, - { field: 'event.dataset', value: 'threatintel.abusemalware' }, - { - field: 'matched.atomic', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', - }, - { field: 'matched.field', value: 'myhash.mysha256' }, - { - field: 'matched.id', - value: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', - }, - { field: 'matched.index', value: 'filebeat-7.12.0-2021.03.10-000001' }, - { field: 'matched.type', value: 'file' }, - ]; - - expandFirstAlert(); - openThreatIndicatorDetails(); - - cy.get(THREAT_INTEL_TAB).should('have.text', 'Threat Intel (1)'); - cy.get(THREAT_DETAILS_VIEW).within(() => { - cy.get(TABLE_ROWS).should('have.length', expectedThreatIndicatorData.length); - expectedThreatIndicatorData.forEach((row, index) => { - cy.get(TABLE_ROWS) - .eq(index) - .within(() => { - cy.get(TABLE_CELL).eq(0).should('have.text', row.field); - cy.get(TABLE_CELL).eq(1).should('have.text', row.value); - }); - }); - }); - }); - }); - describe('Duplicates the indicator rule', () => { beforeEach(() => { cleanKibana(); From 7978796d359c7b6216ced657b821cd6f273de2f8 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 21 Jun 2021 16:42:56 -0500 Subject: [PATCH 03/38] Move test constants into tests that use them --- .../detection_alerts/cti_enrichments.spec.ts | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index 7556b5f4342aa..b2b4a295c01b5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -56,28 +56,6 @@ import { addsFieldsToTimeline } from '../../tasks/rule_details'; // }); describe('CTI Enrichment', () => { - const fieldSearch = 'threat.indicator.matched'; - const fields = [ - 'threat.indicator.matched.atomic', - 'threat.indicator.matched.type', - 'threat.indicator.matched.field', - ]; - const expectedFieldsText = [ - newThreatIndicatorRule.atomic, - newThreatIndicatorRule.type, - newThreatIndicatorRule.indicatorMappingField, - ]; - - const expectedEnrichment = [ - { line: 4, text: ' "threat": {' }, - { - line: 3, - text: - ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', - }, - { line: 2, text: ' }' }, - ]; - before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); @@ -99,15 +77,32 @@ describe('CTI Enrichment', () => { goToRuleDetails(); }); - it('Displays matches on the timeline', () => { - addsFieldsToTimeline(fieldSearch, fields); + it('Displays enrichment matched.* fields on the timeline', () => { + const expectedFields = { + 'threat.indicator.matched.atomic': newThreatIndicatorRule.atomic, + 'threat.indicator.matched.type': newThreatIndicatorRule.type, + 'threat.indicator.matched.field': newThreatIndicatorRule.indicatorMappingField, + }; + const fields = Object.keys(expectedFields) as Array; + + addsFieldsToTimeline('threat.indicator.matched', fields); - fields.forEach((field, index) => { - cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFieldsText[index]); + fields.forEach((field) => { + cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFields[field]); }); }); - it('Displays enrichment on the JSON view', () => { + it('Displays persisted enrichments on the JSON view', () => { + const expectedEnrichment = [ + { line: 4, text: ' "threat": {' }, + { + line: 3, + text: + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + }, + { line: 2, text: ' }' }, + ]; + expandFirstAlert(); openJsonView(); scrollJsonViewToBottom(); From 18b22fcae441fb6d144b822a8b88dad74fe1556a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 22 Jun 2021 12:44:22 -0500 Subject: [PATCH 04/38] style: declare FC function as an FC --- .../common/components/event_details/threat_summary_view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx index 67b09e8e59699..4cb19773d4a27 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx @@ -38,12 +38,12 @@ const getThreatSummaryRows = ( return null; }).filter((item: ThreatSummaryRow | null): item is ThreatSummaryRow => !!item); -const getDescription = ({ +const Description: React.FC = ({ contextId, eventId, fieldName, values, -}: ThreatSummaryRow['description']): JSX.Element => ( +}) => ( <> {values.map((value: string) => ( ); -const summaryColumns: Array> = getSummaryColumns(getDescription); +const summaryColumns: Array> = getSummaryColumns(Description); const ThreatSummaryViewComponent: React.FC<{ data: TimelineEventsDetailsItem[]; From 7d6ffc931970f26473060a2bc9bc46432b2415c4 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 22 Jun 2021 13:48:55 -0500 Subject: [PATCH 05/38] Extract some inline parsing logic into a helper function And test it! --- .../cti_details/helpers.test.tsx | 415 ++++++++++++++++++ .../event_details/cti_details/helpers.tsx | 39 ++ .../event_details/event_details.tsx | 22 +- 3 files changed, 460 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx new file mode 100644 index 0000000000000..2f8ebf62a6ac6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx @@ -0,0 +1,415 @@ +/* + * 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 { parseExistingEnrichments } from './helpers'; + +describe('parseExistingEnrichments', () => { + it('returns an empty array if data is empty', () => { + expect(parseExistingEnrichments([])).toEqual([]); + }); + + it('returns an empty array if data contains no enrichment field', () => { + const data = [ + { + category: 'host', + field: 'host.os.name.text', + isObjectArray: false, + originalValue: ['Mac OS X'], + values: ['Mac OS X'], + }, + ]; + expect(parseExistingEnrichments(data)).toEqual([]); + }); + + it('returns an empty array if enrichment field contains invalid JSON', () => { + const data = [ + { + category: 'threat', + field: 'threat.indicator', + isObjectArray: true, + originalValue: ['whoops'], + values: ['whoops'], + }, + ]; + expect(parseExistingEnrichments(data)).toEqual([]); + }); + + it('returns an array if enrichment field contains valid JSON', () => { + const data = [ + { + category: 'threat', + field: 'threat.indicator', + isObjectArray: true, + originalValue: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + values: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + }, + ]; + + expect(parseExistingEnrichments(data)).toEqual([ + [ + { + category: 'first_seen', + field: 'first_seen', + isObjectArray: false, + originalValue: ['2021-03-21T19:40:19.000Z'], + values: ['2021-03-21T19:40:19.000Z'], + }, + { + category: 'provider', + field: 'provider', + isObjectArray: false, + originalValue: ['geenensp'], + values: ['geenensp'], + }, + { + category: 'ip', + field: 'ip', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'type', + field: 'type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { + category: 'event', + field: 'event.ingested', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:44.213673Z'], + values: ['2021-03-08T19:40:44.213673Z'], + }, + { + category: 'event', + field: 'event.created', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:43.160Z'], + values: ['2021-03-08T19:40:43.160Z'], + }, + { + category: 'event', + field: 'event.kind', + isObjectArray: false, + originalValue: ['other'], + values: ['other'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['threatintel'], + values: ['threatintel'], + }, + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['threat'], + values: ['threat'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['indicator'], + values: ['indicator'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['threatintel.abuseurl'], + values: ['threatintel.abuseurl'], + }, + { + category: 'matched', + field: 'matched.atomic', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'matched', + field: 'matched.field', + isObjectArray: false, + originalValue: ['host.ip'], + values: ['host.ip'], + }, + { + category: 'matched', + field: 'matched.id', + isObjectArray: false, + originalValue: ['0SIZMnoB_Blp1Ib9ZYHU'], + values: ['0SIZMnoB_Blp1Ib9ZYHU'], + }, + { + category: 'matched', + field: 'matched.index', + isObjectArray: false, + originalValue: ['filebeat-8.0.0-2021.05.28-000001'], + values: ['filebeat-8.0.0-2021.05.28-000001'], + }, + { + category: 'matched', + field: 'matched.type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + ], + ]); + }); + + it('returns multiple arrays for multiple enrichments', () => { + const data = [ + { + category: 'threat', + field: 'threat.indicator', + isObjectArray: true, + originalValue: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"other","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"iiL9NHoB_Blp1Ib9yoJo","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + values: [ + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"other","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"iiL9NHoB_Blp1Ib9yoJo","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + `{"first_seen":"2021-03-21T19:40:19.000Z","provider":"geenensp","ip":"192.168.1.19","type":"url","event":{"reference":"https://urlhaus.abuse.ch/url/1055419/","ingested":"2021-03-08T19:40:44.213673Z","created":"2021-03-08T19:40:43.160Z","kind":"other","module":"threatintel","category":"threat","type":"indicator","dataset":"threatintel.abuseurl"},"matched":{"atomic":"192.168.1.19","field":"host.ip","id":"0SIZMnoB_Blp1Ib9ZYHU","index":"filebeat-8.0.0-2021.05.28-000001","type":"url"}}`, + ], + }, + ]; + + expect(parseExistingEnrichments(data)).toEqual([ + expect.arrayContaining([ + { + category: 'first_seen', + field: 'first_seen', + isObjectArray: false, + originalValue: ['2021-03-21T19:40:19.000Z'], + values: ['2021-03-21T19:40:19.000Z'], + }, + { + category: 'provider', + field: 'provider', + isObjectArray: false, + originalValue: ['other'], + values: ['other'], + }, + { + category: 'ip', + field: 'ip', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'type', + field: 'type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { + category: 'event', + field: 'event.ingested', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:44.213673Z'], + values: ['2021-03-08T19:40:44.213673Z'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['threatintel'], + values: ['threatintel'], + }, + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['threat'], + values: ['threat'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['indicator'], + values: ['indicator'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['threatintel.abuseurl'], + values: ['threatintel.abuseurl'], + }, + { + category: 'matched', + field: 'matched.atomic', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'matched', + field: 'matched.field', + isObjectArray: false, + originalValue: ['host.ip'], + values: ['host.ip'], + }, + { + category: 'matched', + field: 'matched.id', + isObjectArray: false, + originalValue: ['iiL9NHoB_Blp1Ib9yoJo'], + values: ['iiL9NHoB_Blp1Ib9yoJo'], + }, + { + category: 'matched', + field: 'matched.index', + isObjectArray: false, + originalValue: ['filebeat-8.0.0-2021.05.28-000001'], + values: ['filebeat-8.0.0-2021.05.28-000001'], + }, + { + category: 'matched', + field: 'matched.type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + ]), + expect.arrayContaining([ + { + category: 'first_seen', + field: 'first_seen', + isObjectArray: false, + originalValue: ['2021-03-21T19:40:19.000Z'], + values: ['2021-03-21T19:40:19.000Z'], + }, + { + category: 'provider', + field: 'provider', + isObjectArray: false, + originalValue: ['geenensp'], + values: ['geenensp'], + }, + { + category: 'ip', + field: 'ip', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'type', + field: 'type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + { + category: 'event', + field: 'event.ingested', + isObjectArray: false, + originalValue: ['2021-03-08T19:40:44.213673Z'], + values: ['2021-03-08T19:40:44.213673Z'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['threatintel'], + values: ['threatintel'], + }, + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['threat'], + values: ['threat'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['indicator'], + values: ['indicator'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['threatintel.abuseurl'], + values: ['threatintel.abuseurl'], + }, + { + category: 'matched', + field: 'matched.atomic', + isObjectArray: false, + originalValue: ['192.168.1.19'], + values: ['192.168.1.19'], + }, + { + category: 'matched', + field: 'matched.field', + isObjectArray: false, + originalValue: ['host.ip'], + values: ['host.ip'], + }, + { + category: 'matched', + field: 'matched.id', + isObjectArray: false, + originalValue: ['0SIZMnoB_Blp1Ib9ZYHU'], + values: ['0SIZMnoB_Blp1Ib9ZYHU'], + }, + { + category: 'matched', + field: 'matched.index', + isObjectArray: false, + originalValue: ['filebeat-8.0.0-2021.05.28-000001'], + values: ['filebeat-8.0.0-2021.05.28-000001'], + }, + { + category: 'matched', + field: 'matched.type', + isObjectArray: false, + originalValue: ['url'], + values: ['url'], + }, + ]), + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx new file mode 100644 index 0000000000000..02df1b40c5fd1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -0,0 +1,39 @@ +/* + * 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'; +import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; + +const isEventDetailsItem = ( + item: TimelineEventsDetailsItem[] | null +): item is TimelineEventsDetailsItem[] => !!item; + +export const parseExistingEnrichments = ( + data: TimelineEventsDetailsItem[] +): TimelineEventsDetailsItem[][] => { + const threatIndicatorField = data.find( + ({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue + ); + if (!threatIndicatorField) { + return []; + } + + const { originalValue } = threatIndicatorField; + const enrichmentStrings = Array.isArray(originalValue) ? originalValue : [originalValue]; + + return enrichmentStrings + .map((enrichmentString) => { + try { + const enrichment = JSON.parse(enrichmentString); + return getDataFromSourceHits(enrichment); + } catch (e) { + return null; + } + }) + .filter(isEventDetailsItem); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index c4092214633e5..f8f9153ec08b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -18,8 +18,7 @@ import { AlertSummaryView } from './alert_summary_view'; import { BrowserFields } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { TimelineTabs } from '../../../../common/types/timeline'; -import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -import { getDataFromSourceHits } from '../../../../common/utils/field_formatters'; +import { parseExistingEnrichments } from './cti_details/helpers'; interface EventViewTab { id: EventViewId; @@ -91,20 +90,11 @@ const EventDetailsComponent: React.FC = ({ [setSelectedTabId] ); - const threatData = useMemo(() => { - if (isAlert && data) { - const threatIndicator = data.find( - ({ field, originalValue }) => field === INDICATOR_DESTINATION_PATH && originalValue - ); - if (!threatIndicator) return []; - const { originalValue } = threatIndicator; - const values = Array.isArray(originalValue) ? originalValue : [originalValue]; - return values.map((value) => getDataFromSourceHits(JSON.parse(value))); - } - return []; - }, [data, isAlert]); - - const threatCount = useMemo(() => threatData.length, [threatData.length]); + const threatData = useMemo(() => (isAlert ? parseExistingEnrichments(data) : []), [ + data, + isAlert, + ]); + const threatCount = threatData.length; const summaryTab = useMemo( () => From 3dafc7cb0dc6cfb814c8f5cfa8695964846983f4 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 23 Jun 2021 19:57:20 -0500 Subject: [PATCH 06/38] Solidifying enrichment types on the backend * Declares an enum for our types * Sets type during indicator match rule enrichment * Sets type during investigation-time enrichment --- .../plugins/security_solution/common/cti/constants.ts | 5 +++++ .../threat_mapping/enrich_signal_threat_matches.ts | 10 ++++++++-- .../factory/cti/event_enrichment/helpers.ts | 6 +++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 10452996eae6f..2b4bd62899b93 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -45,6 +45,11 @@ export const SORTED_THREAT_SUMMARY_FIELDS = [ INDICATOR_LASTSEEN, ]; +export enum ENRICHMENT_TYPES { + InvestigationTime = 'investigation_time', + IndicatorMatchRule = 'indicator_match_rule', +} + export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { 'file.hash.md5': 'threatintel.indicator.file.hash.md5', 'file.hash.sha1': 'threatintel.indicator.file.hash.sha1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index c26f03d1dd480..3423cc1a8744f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -6,6 +6,7 @@ */ import { get, isObject } from 'lodash'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import type { SignalSearchResponse, SignalSourceHit } from '../types'; import type { @@ -56,13 +57,18 @@ export const buildMatchedIndicator = ({ throw new Error(`Expected indicator field to be an object, but found: ${indicator}`); } const atomic = get(matchedThreat?._source, query.value) as unknown; - const type = get(indicator, 'type') as unknown; const event = get(matchedThreat?._source, 'event') as unknown; return { ...indicator, event, - matched: { atomic, field: query.field, id: query.id, index: query.index, type }, + matched: { + atomic, + field: query.field, + id: query.id, + index: query.index, + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, }; }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts index e4ed05baeed77..9ab1114174ff4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts @@ -8,7 +8,10 @@ import { get, isEmpty } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; -import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../../../../common/cti/constants'; +import { + ENRICHMENT_TYPES, + EVENT_ENRICHMENT_INDICATOR_FIELD_MAP, +} from '../../../../../../common/cti/constants'; import { CtiEnrichment } from '../../../../../../common/search_strategy/security_solution/cti'; type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP; @@ -67,6 +70,7 @@ const buildIndicatorMatchedFields = ( 'matched.field': [eventField], 'matched.id': [hit._id], 'matched.index': [hit._index], + 'matched.type': [ENRICHMENT_TYPES.InvestigationTime], }; }; From d7e6f9dd9161cc0f04b82a31aa7738c1f53445ce Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 23 Jun 2021 19:58:36 -0500 Subject: [PATCH 07/38] WIP: Enrichment rows are rendered on the alerts summary There are lots of TODOs here, but this implements the following: * Fetching investigation-time enrichments from the backend * Parsing existing enrichments from timeline data * Merging the two enrichment types together, and rendering them in rows as specified Much of the data-fetching is hardcoded, and this broke the existing pattern with SummaryView/SummaryRow so that got a little messy; I may end up just using my own EuiTable but we'll see. Threat Intel tab is currently broken; that's up next. --- .../security_solution/common/cti/constants.ts | 8 - .../event_details/cti_details/helpers.tsx | 19 ++ .../threat_summary_view.test.tsx | 8 +- .../cti_details/threat_summary_view.tsx | 170 ++++++++++++++++++ .../event_details/cti_details/translations.ts | 45 +++++ .../event_details/event_details.tsx | 80 +++++++-- .../components/event_details/helpers.tsx | 13 +- .../components/event_details/summary_view.tsx | 2 +- .../event_details/threat_summary_view.tsx | 78 -------- .../side_panel/event_details/index.tsx | 2 - 10 files changed, 306 insertions(+), 119 deletions(-) rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/threat_summary_view.test.tsx (80%) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 2b4bd62899b93..391ddb1d123ed 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -37,14 +37,6 @@ export const CTI_ROW_RENDERER_FIELDS = [ INDICATOR_PROVIDER, ]; -export const SORTED_THREAT_SUMMARY_FIELDS = [ - INDICATOR_MATCHED_FIELD, - INDICATOR_MATCHED_TYPE, - INDICATOR_PROVIDER, - INDICATOR_FIRSTSEEN, - INDICATOR_LASTSEEN, -]; - export enum ENRICHMENT_TYPES { InvestigationTime = 'investigation_time', IndicatorMatchRule = 'indicator_match_rule', diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 02df1b40c5fd1..c430f29b96322 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -6,8 +6,11 @@ */ import { INDICATOR_DESTINATION_PATH } from '../../../../../common/constants'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; +import * as i18n from './translations'; const isEventDetailsItem = ( item: TimelineEventsDetailsItem[] | null @@ -37,3 +40,19 @@ export const parseExistingEnrichments = ( }) .filter(isEventDetailsItem); }; + +export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): CtiEnrichment => + data.reduce((acc, item) => { + acc[item.field] = item.originalValue; + return acc; + }, {}); + +export const getTooltipTitle = (type: string | undefined) => + type === ENRICHMENT_TYPES.InvestigationTime + ? i18n.INVESTIGATION_TOOLTIP_TITLE + : i18n.INDICATOR_TOOLTIP_TITLE; + +export const getTooltipContent = (type: string | undefined) => + type === ENRICHMENT_TYPES.InvestigationTime + ? i18n.INVESTIGATION_TOOLTIP_CONTENT + : i18n.INDICATOR_TOOLTIP_CONTENT; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx index fa12ff3db7895..b34f2379c3f17 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { ThreatSummaryView } from './threat_summary_view'; -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { mockAlertDetailsData } from './__mocks__'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { mockAlertDetailsData } from '../__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { return { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx new file mode 100644 index 0000000000000..8cb51fb7b2a75 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -0,0 +1,170 @@ +/* + * 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 styled from 'styled-components'; +import React from 'react'; +import { EuiBasicTableColumn, EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; + +import * as i18n from './translations'; +import { StyledEuiInMemoryTable } from '../summary_view'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import { + MATCHED_ATOMIC, + MATCHED_FIELD, + MATCHED_TYPE, + PROVIDER, +} from '../../../../../common/cti/constants'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; +import { getTooltipTitle, getTooltipContent } from './helpers'; + +const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => + array ? array[0] : undefined; +const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => + getFirstElement(enrichment[field]) as string | undefined; + +export interface ThreatSummaryItem { + title: { + title: string | undefined; + type: string | undefined; + }; + description: { + timelineId: string; + eventId: string; + fieldName: string | undefined; + value: string | undefined; + provider: string | undefined; + }; +} + +// Overrides flex styles declared in StyledEuiInMemoryTable +const FlexContainer = styled.div` + display: flex; + align-items: center; + width: 100%; +`; + +const RightMargin = styled.span` + margin-right: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const EnrichmentTitle: React.FC = ({ title, type }) => ( + + + + + +
{title}
+
+
+); + +const EnrichmentIcon: React.FC<{ type: string | undefined }> = ({ type }) => { + return ( + + + + ); +}; + +const EnrichmentDescription: React.FC = ({ + timelineId, + eventId, + fieldName, + value, + provider, +}) => ( + + + + + {provider && ( + <> + + + {i18n.PROVIDER_PREPOSITION} + + + + + {provider} + + + + )} + +); + +const buildThreatSummaryItems = ( + enrichments: CtiEnrichment[], + timelineId: string, + eventId: string +) => { + return enrichments.map((enrichment) => { + const field = getEnrichmentValue(enrichment, MATCHED_FIELD); + const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); + const type = getEnrichmentValue(enrichment, MATCHED_TYPE); + // This value may be in one of two locations depending on whether it's an + // old indicator alert or a new enrichment. Once enrichment has been + // normalized and we support the new ECS fields, this value should always be + // 'indicator.provider' + const provider = + getEnrichmentValue(enrichment, PROVIDER) || + getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${PROVIDER}`); + + return { + title: { + title: field, + type, + }, + description: { + eventId, + fieldName: field, + provider, + timelineId, + value, + }, + }; + }); +}; + +const columns: Array> = [ + { + field: 'title', + truncateText: false, + render: EnrichmentTitle, + width: '160px', + name: '', + }, + { + field: 'description', + truncateText: false, + render: EnrichmentDescription, + name: '', + }, +]; + +const ThreatSummaryViewComponent: React.FC<{ + enrichments: CtiEnrichment[]; + timelineId: string; + eventId: string; +}> = ({ enrichments, timelineId, eventId }) => ( + +); + +export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts new file mode 100644 index 0000000000000..35a2a02b2f553 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const PROVIDER_PREPOSITION = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.providerPreposition', + { + defaultMessage: 'from', + } +); + +export const INDICATOR_TOOLTIP_TITLE = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle', + { + defaultMessage: 'Indicator rule enrichment', + } +); + +export const INVESTIGATION_TOOLTIP_TITLE = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle', + { + defaultMessage: 'Investigation time enrichment', + } +); + +export const INDICATOR_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent', + { + defaultMessage: + 'This field matched a known indicator, and was enriched by an indicator match rule. See more details on the Threat Intel tab.', + } +); + +export const INVESTIGATION_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent', + { + defaultMessage: + 'This field matched a known indicator; see more details on the Threat Intel tab.', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index f8f9153ec08b4..fdc93f8000d19 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -6,19 +6,22 @@ */ import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; -import { ThreatSummaryView } from './threat_summary_view'; +import { ThreatSummaryView } from './cti_details/threat_summary_view'; import { ThreatDetailsView } from './threat_details_view'; import * as i18n from './translations'; import { AlertSummaryView } from './alert_summary_view'; import { BrowserFields } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { TimelineTabs } from '../../../../common/types/timeline'; -import { parseExistingEnrichments } from './cti_details/helpers'; +import { useAppToasts } from '../../hooks/use_app_toasts'; +import { useKibana } from '../../lib/kibana'; +import { useEventEnrichment } from '../../containers/events/event_enrichment'; +import { parseExistingEnrichments, timelineDataToEnrichment } from './cti_details/helpers'; interface EventViewTab { id: EventViewId; @@ -90,11 +93,54 @@ const EventDetailsComponent: React.FC = ({ [setSelectedTabId] ); - const threatData = useMemo(() => (isAlert ? parseExistingEnrichments(data) : []), [ - data, - isAlert, - ]); - const threatCount = threatData.length; + const { addError } = useAppToasts(); + const kibana = useKibana(); + const [{ from, to }, setRange] = useState({ from: 'now-30d', to: 'now' }); + const { + error: enrichmentError, + loading: enrichmentsLoading, + result: enrichmentsResponse, + start: getEnrichments, + } = useEventEnrichment(); + // console.log('enrichments', enrichmentsLoading, enrichmentsResponse); + // console.log('data', data); + + useEffect(() => { + if (enrichmentError) { + addError(enrichmentError, { title: 'TODO' }); + } + }, [addError, enrichmentError]); + + useEffect(() => { + getEnrichments({ + data: kibana.services.data, + timerange: { from, to, interval: '' }, + defaultIndex: ['*'], // TODO do we apply the current sources here? + eventFields: { + 'source.ip': '192.168.1.19', // TODO get event values + }, + filterQuery: '', // TODO do we apply the current filters here? + }); + }, [from, getEnrichments, kibana.services.data, to]); + + const existingEnrichments = useMemo( + () => + isAlert + ? parseExistingEnrichments(data).map((enrichmentData) => + timelineDataToEnrichment(enrichmentData) + ) + : [], + [data, isAlert] + ); + + const allEnrichments = useMemo(() => { + // TODO dedup + if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { + return existingEnrichments; + } + return [...existingEnrichments, ...enrichmentsResponse.enrichments]; + }, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]); + const enrichmentCount = allEnrichments.length; const summaryTab = useMemo( () => @@ -110,15 +156,21 @@ const EventDetailsComponent: React.FC = ({ eventId: id, browserFields, timelineId, - title: threatCount ? i18n.ALERT_SUMMARY : undefined, + title: i18n.ALERT_SUMMARY, }} /> - {threatCount > 0 && } + {enrichmentCount > 0 && ( + + )} ), } : undefined, - [browserFields, data, id, isAlert, timelineId, threatCount] + [isAlert, data, id, browserFields, timelineId, enrichmentCount, allEnrichments] ); const threatIntelTab = useMemo( @@ -127,11 +179,11 @@ const EventDetailsComponent: React.FC = ({ ? { id: EventsViewType.threatIntelView, 'data-test-subj': 'threatIntelTab', - name: `${i18n.THREAT_INTEL} (${threatCount})`, - content: , + name: `${i18n.THREAT_INTEL} (${enrichmentCount})`, + content: , } : undefined, - [isAlert, threatCount, threatData] + [allEnrichments, enrichmentCount, isAlert] ); const tableTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 8392be420a2c5..6002f66da4309 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -64,16 +64,6 @@ export interface AlertSummaryRow { }; } -export interface ThreatSummaryRow { - title: string; - description: { - contextId: string; - eventId: string; - fieldName: string; - values: string[]; - }; -} - export interface ThreatDetailsRow { title: string; description: { @@ -82,7 +72,7 @@ export interface ThreatDetailsRow { }; } -export type SummaryRow = AlertSummaryRow | ThreatSummaryRow | ThreatDetailsRow; +export type SummaryRow = AlertSummaryRow | ThreatDetailsRow; export const getColumnHeaderFromBrowserField = ({ browserField, @@ -215,7 +205,6 @@ getTitle.displayName = 'getTitle'; export const getSummaryColumns = ( DescriptionComponent: - | React.FC | React.FC | React.FC ): Array> => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 1dda40ae4b19d..542359b9eae5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { SummaryRow } from './helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` +export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` .euiTableHeaderCell { border: none; } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx deleted file mode 100644 index 4cb19773d4a27..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx +++ /dev/null @@ -1,78 +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. - */ - -import { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from './translations'; -import { SummaryView } from './summary_view'; -import { getSummaryColumns, SummaryRow, ThreatSummaryRow } from './helpers'; -import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; -import { SORTED_THREAT_SUMMARY_FIELDS } from '../../../../common/cti/constants'; -import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; - -const getThreatSummaryRows = ( - data: TimelineEventsDetailsItem[], - timelineId: string, - eventId: string -) => - SORTED_THREAT_SUMMARY_FIELDS.map((threatSummaryField) => { - const item = data.find(({ field }) => field === threatSummaryField); - if (item) { - const { field, originalValue } = item; - return { - title: field.replace(`${INDICATOR_DESTINATION_PATH}.`, ''), - description: { - values: Array.isArray(originalValue) ? originalValue : [originalValue], - contextId: timelineId, - eventId, - fieldName: field, - }, - }; - } - return null; - }).filter((item: ThreatSummaryRow | null): item is ThreatSummaryRow => !!item); - -const Description: React.FC = ({ - contextId, - eventId, - fieldName, - values, -}) => ( - <> - {values.map((value: string) => ( - - ))} - -); - -const summaryColumns: Array> = getSummaryColumns(Description); - -const ThreatSummaryViewComponent: React.FC<{ - data: TimelineEventsDetailsItem[]; - timelineId: string; - eventId: string; -}> = ({ data, timelineId, eventId }) => ( - <> - - - -); - -export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index e1c002611d108..76341055f28ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -73,8 +73,6 @@ const EventDetailsPanelComponent: React.FC = ({ skip: !expandedEvent.eventId, }); - // TODO get data here - const [isHostIsolationPanelOpen, setIsHostIsolationPanel] = useState(false); const [isolateAction, setIsolateAction] = useState('isolateHost'); From 7216655b02af73bf0f8a62fecfccb86b5cdde304 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 23 Jun 2021 22:53:21 -0500 Subject: [PATCH 08/38] Updates ThreatDetailsView to accept an array of enrichments The investigation-time enrichments are a little messy because they contain all the non-ECS fields that indicators contain; other than that, this is looking good. Still need to add the new header, and potentially sort the fields. --- .../security_solution/common/cti/constants.ts | 4 +- .../empty_threat_details_view.test.tsx | 4 +- .../empty_threat_details_view.tsx | 3 +- .../event_details/cti_details/helpers.tsx | 6 + .../threat_details_view.test.tsx | 4 +- .../cti_details/threat_details_view.tsx | 111 ++++++++++++++++ .../cti_details/threat_summary_view.tsx | 9 +- .../event_details/cti_details/translations.ts | 19 +++ .../event_details/event_details.tsx | 4 +- .../event_details/threat_details_view.tsx | 122 ------------------ .../components/event_details/translations.ts | 19 --- 11 files changed, 149 insertions(+), 156 deletions(-) rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/empty_threat_details_view.test.tsx (90%) rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/empty_threat_details_view.tsx (96%) rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/threat_details_view.test.tsx (96%) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 391ddb1d123ed..eb3b52d1fe194 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -9,6 +9,7 @@ import { INDICATOR_DESTINATION_PATH } from '../constants'; export const MATCHED_ATOMIC = 'matched.atomic'; export const MATCHED_FIELD = 'matched.field'; +export const MATCHED_ID = 'matched.id'; export const MATCHED_TYPE = 'matched.type'; export const INDICATOR_MATCH_SUBFIELDS = [MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE]; @@ -18,11 +19,12 @@ export const INDICATOR_MATCHED_TYPE = `${INDICATOR_DESTINATION_PATH}.${MATCHED_T export const EVENT_DATASET = 'event.dataset'; export const EVENT_REFERENCE = 'event.reference'; +export const EVENT_URL = 'event.url'; export const PROVIDER = 'provider'; export const FIRSTSEEN = 'first_seen'; export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET}`; -export const INDICATOR_EVENT_URL = `${INDICATOR_DESTINATION_PATH}.event.url`; +export const INDICATOR_EVENT_URL = `${INDICATOR_DESTINATION_PATH}.${EVENT_URL}`; export const INDICATOR_FIRSTSEEN = `${INDICATOR_DESTINATION_PATH}.${FIRSTSEEN}`; export const INDICATOR_LASTSEEN = `${INDICATOR_DESTINATION_PATH}.last_seen`; export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx index b3e70fd17c0e1..e7c106bd30822 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; import { EmptyThreatDetailsView } from './empty_threat_details_view'; jest.mock('../../lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx index c78df92dceb3c..d7e1c4d7754ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/empty_threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx @@ -8,8 +8,9 @@ import { EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; + +import { useKibana } from '../../../lib/kibana'; import * as i18n from './translations'; -import { useKibana } from '../../lib/kibana'; const EmptyThreatDetailsViewContainer = styled.div` display: flex; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index c430f29b96322..ab03f72349028 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -56,3 +56,9 @@ export const getTooltipContent = (type: string | undefined) => type === ENRICHMENT_TYPES.InvestigationTime ? i18n.INVESTIGATION_TOOLTIP_CONTENT : i18n.INDICATOR_TOOLTIP_CONTENT; + +export const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => + array ? array[0] : undefined; + +export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => + getFirstElement(enrichment[field]) as string | undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx rename to x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx index 4b2f56a205042..fa3ca7573cfaf 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { ThreatDetailsView } from './threat_details_view'; -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { return { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx new file mode 100644 index 0000000000000..2aecb41f3720a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -0,0 +1,111 @@ +/* + * 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 { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiToolTip, + EuiLink, +} from '@elastic/eui'; +import React, { Fragment } from 'react'; + +import { StyledEuiInMemoryTable } from '../summary_view'; +import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from '../helpers'; +import { EmptyThreatDetailsView } from './empty_threat_details_view'; +import { + FIRSTSEEN, + EVENT_URL, + EVENT_REFERENCE, + MATCHED_ID, +} from '../../../../../common/cti/constants'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { getEnrichmentValue, getFirstElement } from './helpers'; +import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; + +const getFirstSeen = (enrichment: CtiEnrichment): number => { + const firstSeenValue = getEnrichmentValue(enrichment, FIRSTSEEN); + const firstSeenDate = Date.parse(firstSeenValue ?? 'no date'); + return Number.isInteger(firstSeenDate) ? firstSeenDate : new Date(-1).valueOf(); +}; + +const ThreatDetailsDescription: React.FC = ({ + fieldName, + value, +}) => { + const tooltipChild = [EVENT_URL, EVENT_REFERENCE].includes(fieldName) ? ( + + {value} + + ) : ( + {value} + ); + return ( + + + {fieldName} + + + } + > + {tooltipChild} + + ); +}; + +const columns: Array> = getSummaryColumns(ThreatDetailsDescription); + +const buildThreatDetailsItems = (enrichment: CtiEnrichment) => + Object.keys(enrichment).map((field) => { + const displayField = field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH) + ? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}.`, '') + : field; + + return { + title: displayField, + description: { + fieldName: field, + value: getFirstElement(enrichment[field]), + }, + }; + }); + +const ThreatDetailsViewComponent: React.FC<{ + enrichments: CtiEnrichment[]; +}> = ({ enrichments }) => { + if (enrichments.length < 1) { + return ; + } + + const sortedEnrichments = enrichments.sort((a, b) => getFirstSeen(b) - getFirstSeen(a)); + + return ( + <> + + {sortedEnrichments.map((enrichment, index) => { + const key = getEnrichmentValue(enrichment, MATCHED_ID); + return ( + + + {index < enrichments.length - 1 && } + + ); + })} + + ); +}; + +export const ThreatDetailsView = React.memo(ThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index 8cb51fb7b2a75..a69c76966c5e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -20,12 +20,7 @@ import { } from '../../../../../common/cti/constants'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; -import { getTooltipTitle, getTooltipContent } from './helpers'; - -const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => - array ? array[0] : undefined; -const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => - getFirstElement(enrichment[field]) as string | undefined; +import { getTooltipTitle, getTooltipContent, getEnrichmentValue } from './helpers'; export interface ThreatSummaryItem { title: { @@ -80,7 +75,7 @@ const EnrichmentDescription: React.FC = ({ }) => ( - = ({ id: EventsViewType.threatIntelView, 'data-test-subj': 'threatIntelTab', name: `${i18n.THREAT_INTEL} (${enrichmentCount})`, - content: , + content: , } : undefined, [allEnrichments, enrichmentCount, isAlert] diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx deleted file mode 100644 index 0f577200b7b47..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx +++ /dev/null @@ -1,122 +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. - */ - -import { - EuiBasicTableColumn, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiSpacer, - EuiToolTip, - EuiLink, -} from '@elastic/eui'; -import React from 'react'; - -import { isEmpty } from 'fp-ts/Array'; -import { SummaryView } from './summary_view'; -import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from './helpers'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -import { - FIRSTSEEN, - INDICATOR_EVENT_URL, - INDICATOR_REFERENCE, -} from '../../../../common/cti/constants'; -import { EmptyThreatDetailsView } from './empty_threat_details_view'; - -const ThreatDetailsDescription: React.FC = ({ - fieldName, - value, -}) => { - const tooltipChild = [INDICATOR_EVENT_URL, INDICATOR_REFERENCE].some( - (field) => field === fieldName - ) ? ( - - {value} - - ) : ( - {value} - ); - return ( - - - {fieldName} - - - } - > - {tooltipChild} - - ); -}; - -const summaryColumns: Array> = getSummaryColumns( - ThreatDetailsDescription -); - -const getISOStringFromThreatDataItem = (threatDataItem: TimelineEventsDetailsItem[]) => { - const firstSeen = threatDataItem.find( - (item: TimelineEventsDetailsItem) => item.field === FIRSTSEEN - ); - if (firstSeen) { - const { originalValue } = firstSeen; - const firstSeenValue = Array.isArray(originalValue) ? originalValue[0] : originalValue; - if (!Number.isNaN(Date.parse(firstSeenValue))) { - return firstSeenValue; - } - } - return new Date(-1).toString(); -}; - -const getThreatDetailsRowsArray = (threatData: TimelineEventsDetailsItem[][]) => - threatData - .sort( - (a, b) => - Date.parse(getISOStringFromThreatDataItem(b)) - - Date.parse(getISOStringFromThreatDataItem(a)) - ) - .map((items) => - items.map(({ field, originalValue }) => ({ - title: field, - description: { - fieldName: `${INDICATOR_DESTINATION_PATH}.${field}`, - value: Array.isArray(originalValue) ? originalValue[0] : originalValue, - }, - })) - ); - -const ThreatDetailsViewComponent: React.FC<{ - threatData: TimelineEventsDetailsItem[][]; -}> = ({ threatData }) => { - const threatDetailsRowsArray = getThreatDetailsRowsArray(threatData); - return isEmpty(threatDetailsRowsArray) || isEmpty(threatDetailsRowsArray[0]) ? ( - - ) : ( - <> - {threatDetailsRowsArray.map((summaryRows, index, arr) => { - const key = summaryRows.find((threat) => threat.title === 'matched.id')?.description - .value[0]; - return ( -
- {index === 0 && } - - {index < arr.length - 1 && } -
- ); - })} - - ); -}; - -export const ThreatDetailsView = React.memo(ThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index a28d1976ca940..08174972748da 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -23,25 +23,6 @@ export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetail defaultMessage: 'Threat Summary', }); -export const NO_ENRICHMENT_FOUND = i18n.translate( - 'xpack.securitySolution.alertDetails.noEnrichmentFound', - { - defaultMessage: 'No Threat Intel Enrichment Found', - } -); - -export const IF_CTI_NOT_ENABLED = i18n.translate( - 'xpack.securitySolution.alertDetails.ifCtiNotEnabled', - { - defaultMessage: - "If you haven't enabled any threat intelligence sources and want to learn more about this capability, ", - } -); - -export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', { - defaultMessage: 'please check out our documentation.', -}); - export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.summary.investigationGuide', { From ebd1b9d0f03860cdd71890c453840ec5b49a5a47 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 23 Jun 2021 23:00:09 -0500 Subject: [PATCH 09/38] Sort our details fields This promotes sanity for the user. --- .../cti_details/threat_details_view.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index 2aecb41f3720a..ef4ce844c738e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -65,19 +65,21 @@ const ThreatDetailsDescription: React.FC = ({ const columns: Array> = getSummaryColumns(ThreatDetailsDescription); const buildThreatDetailsItems = (enrichment: CtiEnrichment) => - Object.keys(enrichment).map((field) => { - const displayField = field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH) - ? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}.`, '') - : field; + Object.keys(enrichment) + .sort() + .map((field) => { + const displayField = field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH) + ? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}.`, '') + : field; - return { - title: displayField, - description: { - fieldName: field, - value: getFirstElement(enrichment[field]), - }, - }; - }); + return { + title: displayField, + description: { + fieldName: field, + value: getFirstElement(enrichment[field]), + }, + }; + }); const ThreatDetailsViewComponent: React.FC<{ enrichments: CtiEnrichment[]; From dcfa72cb0dead9aea4c07636951991d2ef1e6448 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 23 Jun 2021 23:23:13 -0500 Subject: [PATCH 10/38] Add "view threat intel data" button This simply opens the threat intel tab. --- .../event_details/event_details.tsx | 41 +++++++++++++++---- .../components/event_details/translations.ts | 4 ++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 297c3446ccf92..d58933d198722 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -5,7 +5,14 @@ * 2.0. */ -import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; +import { + EuiTabbedContent, + EuiTabbedContentTab, + EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -92,6 +99,9 @@ const EventDetailsComponent: React.FC = ({ (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), [setSelectedTabId] ); + const viewThreatIntelTab = useCallback(() => setSelectedTabId(EventsViewType.threatIntelView), [ + setSelectedTabId, + ]); const { addError } = useAppToasts(); const kibana = useKibana(); @@ -160,17 +170,34 @@ const EventDetailsComponent: React.FC = ({ }} /> {enrichmentCount > 0 && ( - + <> + + + + + {i18n.VIEW_CTI_DATA} + + + )} ), } : undefined, - [isAlert, data, id, browserFields, timelineId, enrichmentCount, allEnrichments] + [ + isAlert, + data, + id, + browserFields, + timelineId, + enrichmentCount, + allEnrichments, + viewThreatIntelTab, + ] ); const threatIntelTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 08174972748da..a17ca5e434ace 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -23,6 +23,10 @@ export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetail defaultMessage: 'Threat Summary', }); +export const VIEW_CTI_DATA = i18n.translate('xpack.securitySolution.alertDetails.threatIntelCta', { + defaultMessage: 'View threat intel data', +}); + export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.summary.investigationGuide', { From 1e1690f8961b75b3fefcc3da1b001016545b9891 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 24 Jun 2021 00:17:51 -0500 Subject: [PATCH 11/38] Implement header for threat details sections --- .../cti_details/enrichment_icon.tsx | 19 +++++++ .../event_details/cti_details/helpers.tsx | 17 +++++- .../cti_details/threat_details_view.tsx | 53 +++++++++++++++++-- .../cti_details/threat_summary_view.tsx | 29 ++-------- .../event_details/event_details.tsx | 2 - .../components/event_details/summary_view.tsx | 1 + 6 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx new file mode 100644 index 0000000000000..6a2d453759d37 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx @@ -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. + */ + +import React from 'react'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; + +import { getTooltipTitle, getTooltipContent } from './helpers'; + +export const EnrichmentIcon: React.FC<{ type: string | undefined }> = ({ type }) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index ab03f72349028..0f940a1dc9e52 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -5,8 +5,11 @@ * 2.0. */ -import { INDICATOR_DESTINATION_PATH } from '../../../../../common/constants'; -import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + INDICATOR_DESTINATION_PATH, +} from '../../../../../common/constants'; +import { ENRICHMENT_TYPES, PROVIDER } from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; @@ -62,3 +65,13 @@ export const getFirstElement: (array: T[] | undefined) => T | undef export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => getFirstElement(enrichment[field]) as string | undefined; + +/** + * This value may be in one of two locations depending on whether it's an + * old indicator alert or a new enrichment. Once enrichment has been + * normalized and we support the new ECS fields, this value should always be + * 'indicator.provider'; + */ +export const getEnrichmentProvider = (enrichment: CtiEnrichment) => + getEnrichmentValue(enrichment, PROVIDER) || + getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${PROVIDER}`); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index ef4ce844c738e..9a5870d66b55e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -13,6 +13,8 @@ import { EuiSpacer, EuiToolTip, EuiLink, + EuiText, + EuiTextColor, } from '@elastic/eui'; import React, { Fragment } from 'react'; @@ -24,10 +26,15 @@ import { EVENT_URL, EVENT_REFERENCE, MATCHED_ID, + MATCHED_FIELD, + MATCHED_ATOMIC, + MATCHED_TYPE, } from '../../../../../common/cti/constants'; -import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { getEnrichmentValue, getFirstElement } from './helpers'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { getEnrichmentProvider, getEnrichmentValue, getFirstElement } from './helpers'; +import * as i18n from './translations'; +import { EnrichmentIcon } from './enrichment_icon'; const getFirstSeen = (enrichment: CtiEnrichment): number => { const firstSeenValue = getEnrichmentValue(enrichment, FIRSTSEEN); @@ -35,6 +42,38 @@ const getFirstSeen = (enrichment: CtiEnrichment): number => { return Number.isInteger(firstSeenDate) ? firstSeenDate : new Date(-1).valueOf(); }; +const ThreatDetailsHeader: React.FC<{ + field: string | undefined; + value: string | undefined; + provider: string | undefined; + type: string | undefined; +}> = ({ field, value, provider, type }) => ( + + + + + + + + {field} {value} + + + {provider && ( + <> + + + {i18n.PROVIDER_PREPOSITION} {provider} + + + + )} + + + + + +); + const ThreatDetailsDescription: React.FC = ({ fieldName, value, @@ -95,14 +134,20 @@ const ThreatDetailsViewComponent: React.FC<{ {sortedEnrichments.map((enrichment, index) => { const key = getEnrichmentValue(enrichment, MATCHED_ID); + const field = getEnrichmentValue(enrichment, MATCHED_FIELD); + const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); + const type = getEnrichmentValue(enrichment, MATCHED_TYPE); + const provider = getEnrichmentProvider(enrichment); + return ( + - {index < enrichments.length - 1 && } ); })} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index a69c76966c5e3..e9fae25474ce1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -7,20 +7,15 @@ import styled from 'styled-components'; import React from 'react'; -import { EuiBasicTableColumn, EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiText, EuiTitle } from '@elastic/eui'; import * as i18n from './translations'; import { StyledEuiInMemoryTable } from '../summary_view'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; -import { - MATCHED_ATOMIC, - MATCHED_FIELD, - MATCHED_TYPE, - PROVIDER, -} from '../../../../../common/cti/constants'; +import { MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE } from '../../../../../common/cti/constants'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; -import { getTooltipTitle, getTooltipContent, getEnrichmentValue } from './helpers'; +import { getEnrichmentProvider, getEnrichmentValue } from './helpers'; +import { EnrichmentIcon } from './enrichment_icon'; export interface ThreatSummaryItem { title: { @@ -58,14 +53,6 @@ const EnrichmentTitle: React.FC = ({ title, type })
); -const EnrichmentIcon: React.FC<{ type: string | undefined }> = ({ type }) => { - return ( - - - - ); -}; - const EnrichmentDescription: React.FC = ({ timelineId, eventId, @@ -109,13 +96,7 @@ const buildThreatSummaryItems = ( const field = getEnrichmentValue(enrichment, MATCHED_FIELD); const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); const type = getEnrichmentValue(enrichment, MATCHED_TYPE); - // This value may be in one of two locations depending on whether it's an - // old indicator alert or a new enrichment. Once enrichment has been - // normalized and we support the new ECS fields, this value should always be - // 'indicator.provider' - const provider = - getEnrichmentValue(enrichment, PROVIDER) || - getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${PROVIDER}`); + const provider = getEnrichmentProvider(enrichment); return { title: { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index d58933d198722..e0f1d59aa5dde 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -112,8 +112,6 @@ const EventDetailsComponent: React.FC = ({ result: enrichmentsResponse, start: getEnrichments, } = useEventEnrichment(); - // console.log('enrichments', enrichmentsLoading, enrichmentsResponse); - // console.log('data', data); useEffect(() => { if (enrichmentError) { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 542359b9eae5a..6d4271c42dffd 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -20,6 +20,7 @@ export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` border: none; } + /* TODO is this needed? */ .euiTableCellContent { display: flex; flex-direction: column; From 2a763949d8483e12557c18fe0507443a9b5417d4 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 24 Jun 2021 19:06:07 -0500 Subject: [PATCH 12/38] Add a basic jest "unit" test around ThreatSummaryView --- .../security_solution/cti/index.mock.ts | 55 ++++++++++++++++++- .../cti_details/threat_summary_view.test.tsx | 30 +++++----- .../cti_details/threat_summary_view.tsx | 2 +- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts index f3dee5a21e4c9..7898962b1a72d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.mock.ts @@ -8,6 +8,7 @@ import { IEsSearchResponse } from 'src/plugins/data/public'; import { + CtiEnrichment, CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, @@ -99,11 +100,63 @@ export const buildEventEnrichmentRawResponseMock = (): IEsSearchResponse => ({ }, }); +export const buildEventEnrichmentMock = ( + overrides: Partial = {} +): CtiEnrichment => ({ + '@timestamp': ['2021-05-28T18:33:52.993Z'], + 'agent.ephemeral_id': ['d6b14f65-5bf3-430d-8315-7b5613685979'], + 'agent.hostname': ['rylastic.local'], + 'agent.id': ['ff93aee5-86a1-4a61-b0e6-0cdc313d01b5'], + 'agent.name': ['rylastic.local'], + 'agent.type': ['filebeat'], + 'agent.version': ['8.0.0'], + 'ecs.version': ['1.6.0'], + 'event.category': ['threat'], + 'event.created': ['2021-05-28T18:33:52.993Z'], + 'event.dataset': ['threatintel.abusemalware'], + 'event.ingested': ['2021-05-28T18:33:55.086Z'], + 'event.kind': ['enrichment'], + 'event.module': ['threatintel'], + 'event.reference': [ + 'https://urlhaus-api.abuse.ch/v1/download/15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e/', + ], + 'event.type': ['indicator'], + 'fileset.name': ['abusemalware'], + 'input.type': ['httpjson'], + 'matched.atomic': ['5529de7b60601aeb36f57824ed0e1ae8'], + 'matched.field': ['file.hash.md5'], + 'matched.id': ['31408415b6d5601a92d29b86c2519658f210c194057588ae396d55cc20b3f03d'], + 'matched.index': ['filebeat-8.0.0-2021.05.28-000001'], + 'matched.type': ['investigation_time'], + 'related.hash': [ + '5529de7b60601aeb36f57824ed0e1ae8', + '15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e', + '768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p', + ], + 'service.type': ['threatintel'], + tags: ['threatintel-abusemalware', 'forwarded'], + 'threatintel.indicator.file.hash.md5': ['5529de7b60601aeb36f57824ed0e1ae8'], + 'threatintel.indicator.file.hash.sha256': [ + '15b012e6f626d0f88c2926d2bf4ca394d7b8ee07cc06d2ec05ea76bed3e8a05e', + ], + 'threatintel.indicator.file.hash.ssdeep': [ + '768:NXSFGJ/ooP6FawrB7Bo1MWnF/jRmhJImp:1SFXIqBo1Mwj2p', + ], + 'threatintel.indicator.file.hash.tlsh': [ + 'FFB20B82F6617061C32784E2712F7A46B179B04FD1EA54A0F28CD8E9CFE4CAA1617F1C', + ], + 'threatintel.indicator.file.size': [24738], + 'threatintel.indicator.file.type': ['html'], + 'threatintel.indicator.first_seen': ['2021-05-28T18:33:29.000Z'], + 'threatintel.indicator.type': ['file'], + ...overrides, +}); + export const buildEventEnrichmentResponseMock = ( overrides: Partial = {} ): CtiEventEnrichmentStrategyResponse => ({ ...buildEventEnrichmentRawResponseMock(), - enrichments: [], + enrichments: [buildEventEnrichmentMock()], inspect: { dsl: ['{"mocked": "json"}'] }, totalCount: 0, ...overrides, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx index b34f2379c3f17..bf6c4b9594344 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx @@ -10,34 +10,32 @@ import React from 'react'; import { ThreatSummaryView } from './threat_summary_view'; import { TestProviders } from '../../../mock'; import { useMountAppended } from '../../../utils/use_mount_appended'; -import { mockAlertDetailsData } from '../__mocks__'; -import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; -jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { - return { - useRuleAsync: jest.fn(), - }; -}); - -const props = { - data: mockAlertDetailsData as TimelineEventsDetailsItem[], - eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', - timelineId: 'detections-page', -}; +jest.mock('../../../../timelines/components/timeline/body/renderers/formatted_field'); describe('ThreatSummaryView', () => { const mount = useMountAppended(); + const eventId = '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'; + const timelineId = 'detections-page'; beforeEach(() => { jest.clearAllMocks(); }); - test('render correct items', () => { + it('renders a row for each enrichment', () => { + const enrichments = [ + buildEventEnrichmentMock(), + buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), + ]; const wrapper = mount( - + ); - expect(wrapper.find('[data-test-subj="threat-summary-view"]').exists()).toEqual(true); + + expect(wrapper.find('[data-test-subj="threat-summary-view"] .euiTableRow')).toHaveLength( + enrichments.length + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index e9fae25474ce1..08875819121c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -138,7 +138,7 @@ const ThreatSummaryViewComponent: React.FC<{ ); From 6a2adf30187b2e7b7659156a9fa1a3eb798dcb72 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 24 Jun 2021 20:17:54 -0500 Subject: [PATCH 13/38] Fix remaining tests for components we modified This also addresses a bug where we were not properly sorting new enrichments by first_seen; this is covered under the tests that were fixed. --- .../empty_threat_details_view.test.tsx | 6 +- .../event_details/cti_details/helpers.tsx | 16 +-- .../cti_details/threat_details_view.test.tsx | 131 ++++++------------ .../cti_details/threat_details_view.tsx | 9 +- .../cti_details/threat_summary_view.tsx | 11 +- 5 files changed, 68 insertions(+), 105 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx index e7c106bd30822..76c6b077236f0 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.test.tsx @@ -12,7 +12,7 @@ import { useMountAppended } from '../../../utils/use_mount_appended'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; import { EmptyThreatDetailsView } from './empty_threat_details_view'; -jest.mock('../../lib/kibana'); +jest.mock('../../../lib/kibana'); describe('EmptyThreatDetailsView', () => { const mount = useMountAppended(); @@ -28,10 +28,6 @@ describe('EmptyThreatDetailsView', () => { }, }); - beforeEach(() => { - jest.clearAllMocks(); - }); - test('renders correct items', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 0f940a1dc9e52..afd7a29189de8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -9,7 +9,7 @@ import { DEFAULT_INDICATOR_SOURCE_PATH, INDICATOR_DESTINATION_PATH, } from '../../../../../common/constants'; -import { ENRICHMENT_TYPES, PROVIDER } from '../../../../../common/cti/constants'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; @@ -67,11 +67,11 @@ export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => getFirstElement(enrichment[field]) as string | undefined; /** - * This value may be in one of two locations depending on whether it's an - * old indicator alert or a new enrichment. Once enrichment has been - * normalized and we support the new ECS fields, this value should always be - * 'indicator.provider'; + * These fields (e.g. 'x') may be in one of two keys depending on whether it's + * a new enrichment ('threatintel.indicator.x') or an old indicator alert + * (simply 'x'). Once enrichment has been normalized and we support the new ECS + * fields, this value should always be 'indicator.x'; */ -export const getEnrichmentProvider = (enrichment: CtiEnrichment) => - getEnrichmentValue(enrichment, PROVIDER) || - getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${PROVIDER}`); +export const getShimmedIndicatorValue = (enrichment: CtiEnrichment, field: string) => + getEnrichmentValue(enrichment, field) || + getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${field}`); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx index fa3ca7573cfaf..a272861f9bf84 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx @@ -6,109 +6,70 @@ */ import React from 'react'; - -import { ThreatDetailsView } from './threat_details_view'; +import { mount } from 'enzyme'; import { TestProviders } from '../../../mock'; -import { useMountAppended } from '../../../utils/use_mount_appended'; - -jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { - return { - useRuleAsync: jest.fn(), - }; -}); - -const mostRecentDate = '2021-04-25T18:17:00.000Z'; - -const threatData = [ - [ - { - category: 'matched', - field: 'matched.field', - isObjectArray: false, - originalValue: ['test_field_2'], - values: ['test_field_2'], - }, - { - category: 'first_seen', - field: 'first_seen', - isObjectArray: false, - originalValue: ['2019-04-25T18:17:00.000Z'], - values: ['2019-04-25T18:17:00.000Z'], - }, - { - category: 'event', - field: 'event.reference', - isObjectArray: false, - originalValue: ['https://test.com/'], - values: ['https://test.com/'], - }, - { - category: 'event', - field: 'event.url', - isObjectArray: false, - originalValue: ['https://test2.com/'], - values: ['https://test2.com/'], - }, - ], - [ - { - category: 'first_seen', - field: 'first_seen', - isObjectArray: false, - originalValue: [mostRecentDate], - values: [mostRecentDate], - }, - { - category: 'matched', - field: 'matched.field', - isObjectArray: false, - originalValue: ['test_field'], - values: ['test_field'], - }, - ], -]; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; +import { FIRSTSEEN } from '../../../../../common/cti/constants'; +import { ThreatDetailsView } from './threat_details_view'; describe('ThreatDetailsView', () => { - const mount = useMountAppended(); + it('renders a detail view for each enrichment', () => { + const enrichments = [ + buildEventEnrichmentMock(), + buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), + ]; - beforeEach(() => { - jest.clearAllMocks(); - }); + const wrapper = mount(); - test('render correct items', () => { - const wrapper = mount( - - - + expect(wrapper.find('[data-test-subj^="threat-details-view"]').hostNodes()).toHaveLength( + enrichments.length ); - expect(wrapper.find('[data-test-subj="threat-details-view-0"]').exists()).toEqual(true); }); - test('renders empty view if there are no items', () => { + it('renders an empty view if there are no enrichments', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true); }); - test('renders link for event.url and event.reference', () => { - const wrapper = mount( - - - - ); + it('renders anchor links for event.url and event.reference', () => { + const enrichments = [ + buildEventEnrichmentMock({ + 'event.url': ['http://foo.bar'], + 'event.reference': ['http://foo.baz'], + }), + ]; + const wrapper = mount(); expect(wrapper.find('a').length).toEqual(2); }); - test('orders items by first_seen', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('.euiToolTipAnchor span').at(0).text()).toEqual(mostRecentDate); + it('orders enrichments by first_seen descending', () => { + const mostRecentDate = '2021-04-25T18:17:00.000Z'; + const olderDate = '2021-03-25T18:17:00.000Z'; + // this simulates a legacy enrichment from the old indicator match rule, + // where first_seen is available at the top level + const existingEnrichment = buildEventEnrichmentMock({ + first_seen: [mostRecentDate], + }); + delete existingEnrichment['threatintel.indicator.first_seen']; + const newEnrichment = buildEventEnrichmentMock({ + 'matched.id': ['other.id'], + 'threatintel.indicator.first_seen': [olderDate], + }); + + const wrapper = mount(); + + const firstSeenRows = wrapper + .find('.euiTableRow') + .hostNodes() + .filterWhere((node) => node.text().includes(FIRSTSEEN)); + expect(firstSeenRows.map((node) => node.text())).toEqual([ + `first_seen${mostRecentDate}`, + `first_seen${olderDate}`, + ]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index 9a5870d66b55e..bce8a3eeca571 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -29,15 +29,16 @@ import { MATCHED_FIELD, MATCHED_ATOMIC, MATCHED_TYPE, + PROVIDER, } from '../../../../../common/cti/constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { getEnrichmentProvider, getEnrichmentValue, getFirstElement } from './helpers'; +import { getShimmedIndicatorValue, getEnrichmentValue, getFirstElement } from './helpers'; import * as i18n from './translations'; import { EnrichmentIcon } from './enrichment_icon'; const getFirstSeen = (enrichment: CtiEnrichment): number => { - const firstSeenValue = getEnrichmentValue(enrichment, FIRSTSEEN); + const firstSeenValue = getShimmedIndicatorValue(enrichment, FIRSTSEEN); const firstSeenDate = Date.parse(firstSeenValue ?? 'no date'); return Number.isInteger(firstSeenDate) ? firstSeenDate : new Date(-1).valueOf(); }; @@ -137,7 +138,7 @@ const ThreatDetailsViewComponent: React.FC<{ const field = getEnrichmentValue(enrichment, MATCHED_FIELD); const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); const type = getEnrichmentValue(enrichment, MATCHED_TYPE); - const provider = getEnrichmentProvider(enrichment); + const provider = getShimmedIndicatorValue(enrichment, PROVIDER); return ( @@ -145,7 +146,7 @@ const ThreatDetailsViewComponent: React.FC<{ diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index 08875819121c6..74d6e7d80b812 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -12,9 +12,14 @@ import { EuiBasicTableColumn, EuiText, EuiTitle } from '@elastic/eui'; import * as i18n from './translations'; import { StyledEuiInMemoryTable } from '../summary_view'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; -import { MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE } from '../../../../../common/cti/constants'; +import { + MATCHED_ATOMIC, + MATCHED_FIELD, + MATCHED_TYPE, + PROVIDER, +} from '../../../../../common/cti/constants'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { getEnrichmentProvider, getEnrichmentValue } from './helpers'; +import { getEnrichmentValue, getShimmedIndicatorValue } from './helpers'; import { EnrichmentIcon } from './enrichment_icon'; export interface ThreatSummaryItem { @@ -96,7 +101,7 @@ const buildThreatSummaryItems = ( const field = getEnrichmentValue(enrichment, MATCHED_FIELD); const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); const type = getEnrichmentValue(enrichment, MATCHED_TYPE); - const provider = getEnrichmentProvider(enrichment); + const provider = getShimmedIndicatorValue(enrichment, PROVIDER); return { title: { From c823df1c1c5a6635ef696315c3e2384939afbb33 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 24 Jun 2021 20:54:40 -0500 Subject: [PATCH 14/38] Filter out duplicate investigation-time enrichments Because the enrichment endpoint is dumb and doesn't know about the existing event or its enrichments, we need to merge these together on the client to reduce noise and redundant data. --- .../cti_details/helpers.test.tsx | 32 ++++++++++++++++- .../event_details/cti_details/helpers.tsx | 35 ++++++++++++++++++- .../event_details/event_details.tsx | 9 +++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx index 2f8ebf62a6ac6..7b6762c712d45 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import { parseExistingEnrichments } from './helpers'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; +import { filterDuplicateEnrichments, parseExistingEnrichments } from './helpers'; describe('parseExistingEnrichments', () => { it('returns an empty array if data is empty', () => { @@ -413,3 +415,31 @@ describe('parseExistingEnrichments', () => { ]); }); }); + +describe('filterDuplicateEnrichments', () => { + it('returns an empty array if given one', () => { + expect(filterDuplicateEnrichments([])).toEqual([]); + }); + + it('returns the existing enrichment if given both that and an investigation-time enrichment for the same indicator and field', () => { + const existingEnrichment = buildEventEnrichmentMock({ + 'matched.type': [ENRICHMENT_TYPES.IndicatorMatchRule], + }); + const investigationEnrichment = buildEventEnrichmentMock({ + 'matched.type': [ENRICHMENT_TYPES.InvestigationTime], + }); + expect(filterDuplicateEnrichments([existingEnrichment, investigationEnrichment])).toEqual([ + existingEnrichment, + ]); + }); + + it('includes two enrichments from the same indicator if it matched different fields', () => { + const enrichments = [ + buildEventEnrichmentMock(), + buildEventEnrichmentMock({ + 'matched.field': ['other.field'], + }), + ]; + expect(filterDuplicateEnrichments(enrichments)).toEqual(enrichments); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index afd7a29189de8..b7300ddba58ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -5,11 +5,17 @@ * 2.0. */ +import { groupBy } from 'lodash'; import { DEFAULT_INDICATOR_SOURCE_PATH, INDICATOR_DESTINATION_PATH, } from '../../../../../common/constants'; -import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { + ENRICHMENT_TYPES, + MATCHED_FIELD, + MATCHED_ID, + MATCHED_TYPE, +} from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; @@ -75,3 +81,30 @@ export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => export const getShimmedIndicatorValue = (enrichment: CtiEnrichment, field: string) => getEnrichmentValue(enrichment, field) || getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${field}`); + +const buildEnrichmentId = (enrichment: CtiEnrichment): string => { + const matchedId = getEnrichmentValue(enrichment, MATCHED_ID); + const matchedField = getEnrichmentValue(enrichment, MATCHED_FIELD); + return `${matchedId}${matchedField}`; +}; +/** + * This function receives an array of enrichments and removes + * investigation-time enrichments if that exact indicator already exists + * elsewhere in the list. + * + * @param enrichments {@type CtiEnrichment[]} + */ +export const filterDuplicateEnrichments = (enrichments: CtiEnrichment[]): CtiEnrichment[] => { + if (enrichments.length < 2) { + return enrichments; + } + const enrichmentsById = groupBy(enrichments, buildEnrichmentId); + + return Object.values(enrichmentsById).map( + (enrichmentGroup) => + enrichmentGroup.find( + (enrichment) => + getEnrichmentValue(enrichment, MATCHED_TYPE) !== ENRICHMENT_TYPES.InvestigationTime + ) ?? enrichmentGroup[0] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index e0f1d59aa5dde..cb6c2ec26b1c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -28,7 +28,11 @@ import { TimelineTabs } from '../../../../common/types/timeline'; import { useAppToasts } from '../../hooks/use_app_toasts'; import { useKibana } from '../../lib/kibana'; import { useEventEnrichment } from '../../containers/events/event_enrichment'; -import { parseExistingEnrichments, timelineDataToEnrichment } from './cti_details/helpers'; +import { + filterDuplicateEnrichments, + parseExistingEnrichments, + timelineDataToEnrichment, +} from './cti_details/helpers'; interface EventViewTab { id: EventViewId; @@ -142,11 +146,10 @@ const EventDetailsComponent: React.FC = ({ ); const allEnrichments = useMemo(() => { - // TODO dedup if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { return existingEnrichments; } - return [...existingEnrichments, ...enrichmentsResponse.enrichments]; + return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]); }, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]); const enrichmentCount = allEnrichments.length; From 57e5fe3175dfb44cc8e0bb4501b4c515ce806cf1 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 24 Jun 2021 23:07:17 -0500 Subject: [PATCH 15/38] Add inspect button to investigation enrichments * Massages the response into the format that the inspect component uses * Moves stateful fetching of query and persisting in redux to new, more specialized hook * Moves existing enrichment hook to a more suitable location in containers/ --- .../security_solution/cti/index.ts | 2 +- .../event_details/cti_details/helpers.tsx | 10 +-- .../cti_details/threat_details_view.tsx | 69 +++++++++++------- .../event_details/cti_details/translations.ts | 7 ++ .../event_details/event_details.tsx | 31 +------- .../{events => cti}/event_enrichment/api.ts | 0 .../{events => cti}/event_enrichment/index.ts | 1 + .../event_enrichment/use_event_enrichment.ts | 0 .../use_investigation_enrichment.ts | 72 +++++++++++++++++++ 9 files changed, 133 insertions(+), 59 deletions(-) rename x-pack/plugins/security_solution/public/common/containers/{events => cti}/event_enrichment/api.ts (100%) rename x-pack/plugins/security_solution/public/common/containers/{events => cti}/event_enrichment/index.ts (85%) rename x-pack/plugins/security_solution/public/common/containers/{events => cti}/event_enrichment/use_event_enrichment.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 788a44bc5b9f7..79425f3c973b5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -21,6 +21,6 @@ export type CtiEnrichment = Record; export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse { enrichments: CtiEnrichment[]; - inspect?: Inspect; + inspect: Inspect | null; totalCount: number; } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index b7300ddba58ce..0314e4c55f6b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -25,6 +25,9 @@ const isEventDetailsItem = ( item: TimelineEventsDetailsItem[] | null ): item is TimelineEventsDetailsItem[] => !!item; +export const isInvestigationTimeEnrichment = (type: string | undefined) => + type === ENRICHMENT_TYPES.InvestigationTime; + export const parseExistingEnrichments = ( data: TimelineEventsDetailsItem[] ): TimelineEventsDetailsItem[][] => { @@ -57,12 +60,12 @@ export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): Cti }, {}); export const getTooltipTitle = (type: string | undefined) => - type === ENRICHMENT_TYPES.InvestigationTime + isInvestigationTimeEnrichment(type) ? i18n.INVESTIGATION_TOOLTIP_TITLE : i18n.INDICATOR_TOOLTIP_TITLE; export const getTooltipContent = (type: string | undefined) => - type === ENRICHMENT_TYPES.InvestigationTime + isInvestigationTimeEnrichment(type) ? i18n.INVESTIGATION_TOOLTIP_CONTENT : i18n.INDICATOR_TOOLTIP_CONTENT; @@ -103,8 +106,7 @@ export const filterDuplicateEnrichments = (enrichments: CtiEnrichment[]): CtiEnr return Object.values(enrichmentsById).map( (enrichmentGroup) => enrichmentGroup.find( - (enrichment) => - getEnrichmentValue(enrichment, MATCHED_TYPE) !== ENRICHMENT_TYPES.InvestigationTime + (enrichment) => !isInvestigationTimeEnrichment(getEnrichmentValue(enrichment, MATCHED_TYPE)) ) ?? enrichmentGroup[0] ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index bce8a3eeca571..e21e6a5c2e92f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -33,9 +33,16 @@ import { } from '../../../../../common/cti/constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { getShimmedIndicatorValue, getEnrichmentValue, getFirstElement } from './helpers'; +import { + getShimmedIndicatorValue, + getEnrichmentValue, + getFirstElement, + isInvestigationTimeEnrichment, +} from './helpers'; import * as i18n from './translations'; import { EnrichmentIcon } from './enrichment_icon'; +import { QUERY_ID } from '../../../containers/cti/event_enrichment/use_investigation_enrichment'; +import { InspectButton } from '../../inspect'; const getFirstSeen = (enrichment: CtiEnrichment): number => { const firstSeenValue = getShimmedIndicatorValue(enrichment, FIRSTSEEN); @@ -49,30 +56,39 @@ const ThreatDetailsHeader: React.FC<{ provider: string | undefined; type: string | undefined; }> = ({ field, value, provider, type }) => ( - - - - - - - - {field} {value} - - - {provider && ( - <> - - - {i18n.PROVIDER_PREPOSITION} {provider} - - - - )} - - - - - + <> + + + + + + + + {field} {value} + + + {provider && ( + <> + + + {i18n.PROVIDER_PREPOSITION} {provider} + + + + )} + + + + + + {isInvestigationTimeEnrichment(type) && ( + + + + + + )} + ); const ThreatDetailsDescription: React.FC = ({ @@ -132,7 +148,7 @@ const ThreatDetailsViewComponent: React.FC<{ return ( <> - + {sortedEnrichments.map((enrichment, index) => { const key = getEnrichmentValue(enrichment, MATCHED_ID); const field = getEnrichmentValue(enrichment, MATCHED_FIELD); @@ -149,6 +165,7 @@ const ThreatDetailsViewComponent: React.FC<{ data-test-subj={`threat-details-view-${index}`} items={buildThreatDetailsItems(enrichment)} /> + {index < sortedEnrichments.length - 1 && } ); })} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts index 44f1f7911425f..a0c247db927ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -62,3 +62,10 @@ export const IF_CTI_NOT_ENABLED = i18n.translate( export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', { defaultMessage: 'please check out our documentation.', }); + +export const INVESTIGATION_QUERY_TITLE = i18n.translate( + 'xpack.securitySolution.alertDetails.investigationTimeQueryTitle', + { + defaultMessage: 'Investigation time enrichment', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index cb6c2ec26b1c6..b7ca455444daf 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { EventFieldsBrowser } from './event_fields_browser'; @@ -23,11 +23,9 @@ import { ThreatDetailsView } from './cti_details/threat_details_view'; import * as i18n from './translations'; import { AlertSummaryView } from './alert_summary_view'; import { BrowserFields } from '../../containers/source'; +import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { TimelineTabs } from '../../../../common/types/timeline'; -import { useAppToasts } from '../../hooks/use_app_toasts'; -import { useKibana } from '../../lib/kibana'; -import { useEventEnrichment } from '../../containers/events/event_enrichment'; import { filterDuplicateEnrichments, parseExistingEnrichments, @@ -107,33 +105,10 @@ const EventDetailsComponent: React.FC = ({ setSelectedTabId, ]); - const { addError } = useAppToasts(); - const kibana = useKibana(); - const [{ from, to }, setRange] = useState({ from: 'now-30d', to: 'now' }); const { - error: enrichmentError, loading: enrichmentsLoading, result: enrichmentsResponse, - start: getEnrichments, - } = useEventEnrichment(); - - useEffect(() => { - if (enrichmentError) { - addError(enrichmentError, { title: 'TODO' }); - } - }, [addError, enrichmentError]); - - useEffect(() => { - getEnrichments({ - data: kibana.services.data, - timerange: { from, to, interval: '' }, - defaultIndex: ['*'], // TODO do we apply the current sources here? - eventFields: { - 'source.ip': '192.168.1.19', // TODO get event values - }, - filterQuery: '', // TODO do we apply the current filters here? - }); - }, [from, getEnrichments, kibana.services.data, to]); + } = useInvestigationTimeEnrichment(); const existingEnrichments = useMemo( () => diff --git a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/api.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/api.ts rename to x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts diff --git a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts similarity index 85% rename from x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts rename to x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts index 4ce16a2b0e542..e8fb1a03045d9 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts @@ -6,3 +6,4 @@ */ export * from './use_event_enrichment'; +export * from './use_investigation_enrichment'; diff --git a/x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/use_event_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/containers/events/event_enrichment/use_event_enrichment.ts rename to x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts new file mode 100644 index 0000000000000..0036d6711de36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -0,0 +1,72 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useKibana } from '../../../lib/kibana'; +import { useEventEnrichment } from '.'; +import { inputsActions } from '../../../store/actions'; + +export const QUERY_ID = 'investigation_time_enrichment'; +const noop = () => {}; + +export const useInvestigationTimeEnrichment = () => { + const { addError } = useAppToasts(); + const kibana = useKibana(); + const dispatch = useDispatch(); + const [{ from, to }, setRange] = useState({ from: 'now-30d', to: 'now' }); + const { error, loading, result, start } = useEventEnrichment(); + + const deleteQuery = useCallback(() => { + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: QUERY_ID })); + }, [dispatch]); + + useEffect(() => { + if (!loading && result) { + dispatch( + inputsActions.setQuery({ + inputId: 'global', + id: QUERY_ID, + inspect: { + dsl: result.inspect?.dsl ?? [], + response: [JSON.stringify(result.rawResponse, null, 2)], + }, + loading, + refetch: noop, + }) + ); + } + + return deleteQuery; + }, [deleteQuery, dispatch, loading, result]); + + useEffect(() => { + if (error) { + addError(error, { title: 'TODO' }); + } + }, [addError, error]); + + useEffect(() => { + start({ + data: kibana.services.data, + timerange: { from, to, interval: '' }, + defaultIndex: ['filebeat-*'], // TODO do we apply the current sources here? + eventFields: { + 'source.ip': '192.168.1.19', // TODO get event values + }, + filterQuery: '', // TODO do we apply the current filters here? + }); + }, [from, start, kibana.services.data, to]); + + return { + loading, + result, + setRange, + }; +}; From 0bc1eafa4d21e5d6c26792311646a806b7458732 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sat, 26 Jun 2021 16:30:36 -0500 Subject: [PATCH 16/38] Fix failing unit tests * indicator match rule now specifies `matched.type` as coming from the rule * Inspecting the enrichment query requires use of the redux store, which was not previously mocked --- .../cti_details/threat_details_view.test.tsx | 19 ++++++++++--- .../enrich_signal_threat_matches.test.ts | 27 ++++++++++--------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx index a272861f9bf84..0113dde96a4b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx @@ -20,7 +20,11 @@ describe('ThreatDetailsView', () => { buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), ]; - const wrapper = mount(); + const wrapper = mount( + + + + ); expect(wrapper.find('[data-test-subj^="threat-details-view"]').hostNodes()).toHaveLength( enrichments.length @@ -43,7 +47,11 @@ describe('ThreatDetailsView', () => { 'event.reference': ['http://foo.baz'], }), ]; - const wrapper = mount(); + const wrapper = mount( + + + + ); expect(wrapper.find('a').length).toEqual(2); }); @@ -60,8 +68,13 @@ describe('ThreatDetailsView', () => { 'matched.id': ['other.id'], 'threatintel.indicator.first_seen': [olderDate], }); + const enrichments = [existingEnrichment, newEnrichment]; - const wrapper = mount(); + const wrapper = mount( + + + + ); const firstSeenRows = wrapper .find('.euiTableRow') diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 7c80572f6b1ee..4a51880e0f227 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -7,6 +7,7 @@ import { get } from 'lodash'; import { INDICATOR_DESTINATION_PATH } from '../../../../../common/constants'; +import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import { getThreatListItemMock } from './build_threat_mapping_filter.mock'; import { @@ -158,14 +159,14 @@ describe('buildMatchedIndicator', () => { expect(get(indicator, 'matched.field')).toEqual('event.field'); }); - it('returns the type of the matched indicator as matched.type', () => { + it('returns the type of the enrichment as an indicator match type', () => { const [indicator] = buildMatchedIndicator({ queries, threats, indicatorPath, }); - expect(get(indicator, 'matched.type')).toEqual('type_1'); + expect(get(indicator, 'matched.type')).toEqual(ENRICHMENT_TYPES.IndicatorMatchRule); }); it('returns indicators for each provided query', () => { @@ -216,7 +217,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'other_1', type: 'type_1', @@ -263,7 +264,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: 'indicator_type', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, type: 'indicator_type', event: { @@ -294,7 +295,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: undefined, + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, }, ]); @@ -321,7 +322,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: undefined, + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, }, ]); @@ -359,7 +360,7 @@ describe('buildMatchedIndicator', () => { id: '123', index: 'threat-index', field: 'event.field', - type: 'first', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, type: 'first', event: { @@ -478,7 +479,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'other_1', type: 'type_1', @@ -510,7 +511,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: undefined, + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, }, ]); @@ -543,7 +544,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'other_1', type: 'type_1', @@ -608,7 +609,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'custom_index', field: 'event.field', - type: 'custom_type', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, other: 'custom_other', type: 'custom_type', @@ -670,7 +671,7 @@ describe('enrichSignalThreatMatches', () => { id: '123', index: 'indicator_index', field: 'event.field', - type: 'type_1', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, event: { category: 'threat', @@ -685,7 +686,7 @@ describe('enrichSignalThreatMatches', () => { id: '456', index: 'other_custom_index', field: 'event.other', - type: 'type_2', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, event: { category: 'bad', From 67033bc429a03195b898dea00103bcc6d3f52b32 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sat, 26 Jun 2021 17:32:37 -0500 Subject: [PATCH 17/38] Fix existing CTI cypress tests This covers the basics of the Alert Summary and Threat Intel tabs; the investigation-time enrichment functionality is up next. --- .../detection_alerts/cti_enrichments.spec.ts | 61 ++++++++++--------- .../cypress/screens/alerts_details.ts | 2 +- .../es_archives/threat_indicator/data.json | 1 - 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index b2b4a295c01b5..5056b8cfa29e3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -80,7 +80,7 @@ describe('CTI Enrichment', () => { it('Displays enrichment matched.* fields on the timeline', () => { const expectedFields = { 'threat.indicator.matched.atomic': newThreatIndicatorRule.atomic, - 'threat.indicator.matched.type': newThreatIndicatorRule.type, + 'threat.indicator.matched.type': 'indicator_match_rule', 'threat.indicator.matched.field': newThreatIndicatorRule.indicatorMappingField, }; const fields = Object.keys(expectedFields) as Array; @@ -98,7 +98,7 @@ describe('CTI Enrichment', () => { { line: 3, text: - ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"indicator_match_rule\\"}}"', }, { line: 2, text: ' }' }, ]; @@ -117,18 +117,19 @@ describe('CTI Enrichment', () => { }); }); - it('Displays threat summary data on alerts details', () => { - const expectedThreatSummary = [ - { field: 'matched.field', value: 'myhash.mysha256' }, - { field: 'matched.type', value: 'file' }, - { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, + it('Displays a summary of matched fields on the Alerts Summary tab', () => { + const expectedMatches = [ + { + field: 'myhash.mysha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }, ]; expandFirstAlert(); cy.get(THREAT_SUMMARY_VIEW).within(() => { - cy.get(TABLE_ROWS).should('have.length', expectedThreatSummary.length); - expectedThreatSummary.forEach((row, index) => { + cy.get(TABLE_ROWS).should('have.length', expectedMatches.length); + expectedMatches.forEach((row, index) => { cy.get(TABLE_ROWS) .eq(index) .within(() => { @@ -139,37 +140,36 @@ describe('CTI Enrichment', () => { }); }); - it('Displays threat indicator data on the threat intel tab', () => { + it('Displays threat indicator details on the threat intel tab', () => { const expectedThreatIndicatorData = [ - { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, - { field: 'file.size', value: '80280' }, - { field: 'file.type', value: 'elf' }, + { field: 'event.category', value: 'threat' }, + { field: 'event.created', value: '2021-03-10T14:51:07.663Z' }, + { field: 'event.dataset', value: 'threatintel.abusemalware' }, + { field: 'event.ingested', value: '2021-03-10T14:51:09.809069Z' }, + { field: 'event.kind', value: 'enrichment' }, + { field: 'event.module', value: 'threatintel' }, { - field: 'file.hash.sha256', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + field: 'event.reference', + value: + 'https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/(opens in a new tab or window)', }, + { field: 'event.type', value: 'indicator' }, + { field: 'file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, { - field: 'file.hash.tlsh', - value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', + field: 'file.hash.sha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', }, { field: 'file.hash.ssdeep', value: '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', }, - { field: 'file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, - { field: 'type', value: 'file' }, { - field: 'event.reference', - value: - 'https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/(opens in a new tab or window)', + field: 'file.hash.tlsh', + value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', }, - { field: 'event.ingested', value: '2021-03-10T14:51:09.809069Z' }, - { field: 'event.created', value: '2021-03-10T14:51:07.663Z' }, - { field: 'event.kind', value: 'enrichment' }, - { field: 'event.module', value: 'threatintel' }, - { field: 'event.category', value: 'threat' }, - { field: 'event.type', value: 'indicator' }, - { field: 'event.dataset', value: 'threatintel.abusemalware' }, + { field: 'file.size', value: '80280' }, + { field: 'file.type', value: 'elf' }, + { field: 'first_seen', value: '2021-03-10T08:02:14.000Z' }, { field: 'matched.atomic', value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', @@ -180,7 +180,8 @@ describe('CTI Enrichment', () => { value: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', }, { field: 'matched.index', value: 'filebeat-7.12.0-2021.03.10-000001' }, - { field: 'matched.type', value: 'file' }, + { field: 'matched.type', value: 'indicator_match_rule' }, + { field: 'type', value: 'file' }, ]; expandFirstAlert(); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 12bef09b8356d..460652cf6f2da 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -19,7 +19,7 @@ export const TABLE_TAB = '[data-test-subj="tableTab"]'; export const TABLE_ROWS = '.euiTableRow'; -export const THREAT_CONTENT = '[data-test-subj^=draggable-content-threat]'; +export const THREAT_CONTENT = '[data-test-subj^=draggable-content-]'; export const THREAT_DETAILS_VIEW = '[data-test-subj="threat-details-view-0"]'; diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index 9573372d02e9c..c5d382194027f 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -47,7 +47,6 @@ "input": { "type": "httpjson" }, - "@timestamp": "2021-03-10T14:51:07.663Z", "ecs": { "version": "1.6.0" }, From 35eb55070407ce1132dd09d1c7ab3a5dc40b9ed2 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 27 Jun 2021 22:14:27 -0500 Subject: [PATCH 18/38] Adds a cypress test exercising investigation time enrichment * Loads more indicators (filebeat data, `threat_indicator2` archive) AFTER the rule has executed * Asserts that those indicators are also found on the alert summary. --- .../detection_alerts/cti_enrichments.spec.ts | 88 +- .../es_archives/threat_indicator2/data.json | 76 ++ .../threat_indicator2/mappings.json | 822 ++++++++++++++++++ 3 files changed, 937 insertions(+), 49 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json create mode 100644 x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index 5056b8cfa29e3..5aadc1c279b08 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { indexPatterns, newThreatIndicatorRule } from '../../objects/rule'; +import { newThreatIndicatorRule } from '../../objects/rule'; import { cleanKibana, reload } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -21,14 +21,8 @@ import { } from '../../screens/alerts_details'; import { TIMELINE_FIELD } from '../../screens/rule_details'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { - expandFirstAlert, - goToManageAlertsDetectionRules, - investigateFirstAlertInTimeline, - waitForAlertsIndexToBeCreated, - waitForAlertsPanelToBeLoaded, -} from '../../tasks/alerts'; -import { createCustomRuleActivated, createCustomIndicatorRule } from '../../tasks/api_calls/rules'; +import { expandFirstAlert, goToManageAlertsDetectionRules } from '../../tasks/alerts'; +import { createCustomIndicatorRule } from '../../tasks/api_calls/rules'; import { openJsonView, openThreatIndicatorDetails, @@ -38,23 +32,6 @@ import { import { DETECTIONS_URL } from '../../urls/navigation'; import { addsFieldsToTimeline } from '../../tasks/rule_details'; -// describe('CTI enrichments', () => { -// beforeEach(() => { -// cleanKibana(); -// esArchiverLoad('unmapped_fields'); -// loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); -// waitForAlertsPanelToBeLoaded(); -// waitForAlertsIndexToBeCreated(); -// // createCustomRuleActivated(unmappedRule); -// loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); -// waitForAlertsPanelToBeLoaded(); -// expandFirstAlert(); -// }); - -// it('displays enrichments from indicator match rules and from investigation time'); -// it('displays investigation time enrichments from a longer time range'); -// }); - describe('CTI Enrichment', () => { before(() => { cleanKibana(); @@ -117,29 +94,6 @@ describe('CTI Enrichment', () => { }); }); - it('Displays a summary of matched fields on the Alerts Summary tab', () => { - const expectedMatches = [ - { - field: 'myhash.mysha256', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', - }, - ]; - - expandFirstAlert(); - - cy.get(THREAT_SUMMARY_VIEW).within(() => { - cy.get(TABLE_ROWS).should('have.length', expectedMatches.length); - expectedMatches.forEach((row, index) => { - cy.get(TABLE_ROWS) - .eq(index) - .within(() => { - cy.get(TITLE).should('have.text', row.field); - cy.get(THREAT_CONTENT).should('have.text', row.value); - }); - }); - }); - }); - it('Displays threat indicator details on the threat intel tab', () => { const expectedThreatIndicatorData = [ { field: 'event.category', value: 'threat' }, @@ -200,4 +154,40 @@ describe('CTI Enrichment', () => { }); }); }); + + describe('with additional indicators', () => { + before(() => { + esArchiverLoad('threat_indicator2'); + }); + + after(() => { + esArchiverUnload('threat_indicator2'); + }); + + it('Displays matched fields from both indicator match rules and investigation time enrichments on Alerts Summary tab', () => { + const indicatorMatchRuleEnrichment = { + field: 'myhash.mysha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }; + const investigationTimeEnrichment = { + field: 'myhash.mysha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }; + const expectedMatches = [indicatorMatchRuleEnrichment, investigationTimeEnrichment]; + + expandFirstAlert(); + + cy.get(THREAT_SUMMARY_VIEW).within(() => { + cy.get(TABLE_ROWS).should('have.length', expectedMatches.length); + expectedMatches.forEach((row, index) => { + cy.get(TABLE_ROWS) + .eq(index) + .within(() => { + cy.get(TITLE).should('have.text', row.field); + cy.get(THREAT_CONTENT).should('have.text', row.value); + }); + }); + }); + }); + }); }); diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json new file mode 100644 index 0000000000000..170691caa864d --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json @@ -0,0 +1,76 @@ +{ + "type": "doc", + "value": { + "id": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "index": "filebeat-7.12.0-2021.03.11-000001", + "source": { + "@timestamp": "2021-03-11T14:51:05.766Z", + "agent": { + "ephemeral_id": "34c78500-8df5-4a07-ba87-1cc738b98431", + "hostname": "test", + "id": "08a3d064-8f23-41f3-84b2-f917f6ff9588", + "name": "test", + "type": "filebeat", + "version": "7.12.0" + }, + "fileset": { + "name": "abusemalware" + }, + "threatintel": { + "indicator": { + "first_seen": "2021-03-11T08:02:14.000Z", + "file": { + "size": 80280, + "pe": {}, + "type": "elf", + "hash": { + "sha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "tlsh": "6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE", + "ssdeep": "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL", + "md5": "9b6c3518a91d23ed77504b5416bfb5b3" + } + }, + "provider": "another_provider", + "type": "file" + }, + "abusemalware": { + "virustotal": { + "result": "38 / 61", + "link": "https://www.virustotal.com/gui/file/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/detection/f-a04ac6d", + "percent": "62.30" + } + } + }, + "tags": [ + "threatintel-abusemalware", + "forwarded" + ], + "input": { + "type": "httpjson" + }, + "ecs": { + "version": "1.6.0" + }, + "related": { + "hash": [ + "9b6c3518a91d23ed77504b5416bfb5b3", + "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL" + ] + }, + "service": { + "type": "threatintel" + }, + "event": { + "reference": "https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/", + "ingested": "2021-03-11T14:51:09.809069Z", + "created": "2021-03-11T14:51:07.663Z", + "kind": "enrichment", + "module": "threatintel", + "category": "threat", + "type": "indicator", + "dataset": "threatintel.abusemalware" + } + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json new file mode 100644 index 0000000000000..efd23c5a6bba4 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json @@ -0,0 +1,822 @@ +{ + "type": "index", + "value": { + "aliases": { + "filebeat-7.12.0": { + "is_write_index": true + } + }, + "index": "filebeat-7.12.0-2021.03.10-000001", + "mappings": { + "_meta": { + "beat": "filebeat", + "version": "7.12.0" + }, + "date_detection": false, + "dynamic_templates": [ + { + "labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "labels.*" + } + }, + { + "container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "container.labels.*" + } + }, + { + "fields": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "fields.*" + } + }, + { + "docker.container.labels": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.container.labels.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "kubernetes.service.selectors.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.service.selectors.*" + } + }, + { + "docker.attrs": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "docker.attrs.*" + } + }, + { + "azure.activitylogs.identity.claims.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.activitylogs.identity.claims.*" + } + }, + { + "azure.platformlogs.properties.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "azure.platformlogs.properties.*" + } + }, + { + "kibana.log.meta": { + "mapping": { + "type": "keyword" + }, + "match_mapping_type": "string", + "path_match": "kibana.log.meta.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "apache": { + "properties": { + "access": { + "properties": { + "ssl": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "module": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "fileset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "threatintel": { + "properties": { + "abusemalware": { + "properties": { + "file_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_download": { + "ignore_above": 1024, + "type": "keyword" + }, + "virustotal": { + "properties": { + "link": { + "ignore_above": 1024, + "type": "keyword" + }, + "percent": { + "type": "float" + }, + "result": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "abuseurl": { + "properties": { + "blacklists": { + "properties": { + "spamhaus_dbl": { + "ignore_above": 1024, + "type": "keyword" + }, + "surbl": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "larted": { + "type": "boolean" + }, + "reporter": { + "ignore_above": 1024, + "type": "keyword" + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "ignore_above": 1024, + "type": "keyword" + }, + "url_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "urlhaus_reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "anomali": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "labels": { + "ignore_above": 1024, + "type": "keyword" + }, + "modified": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_marking_refs": { + "ignore_above": 1024, + "type": "keyword" + }, + "pattern": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "valid_from": { + "type": "date" + } + } + }, + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + }, + "tlsh": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "imphash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "strings": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "misp": { + "properties": { + "attribute": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "comment": { + "ignore_above": 1024, + "type": "keyword" + }, + "deleted": { + "type": "boolean" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "type": "long" + }, + "event_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "object_relation": { + "ignore_above": 1024, + "type": "keyword" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "to_ids": { + "type": "boolean" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "attribute_count": { + "type": "long" + }, + "date": { + "type": "date" + }, + "disable_correlation": { + "type": "boolean" + }, + "distribution": { + "ignore_above": 1024, + "type": "keyword" + }, + "extends_uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "info": { + "ignore_above": 1024, + "type": "keyword" + }, + "locked": { + "type": "boolean" + }, + "org": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "org_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "orgc": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "local": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "orgc_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "proposal_email_lock": { + "type": "boolean" + }, + "publish_timestamp": { + "type": "date" + }, + "published": { + "type": "boolean" + }, + "sharing_group_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat_level_id": { + "type": "long" + }, + "timestamp": { + "type": "date" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "otx": { + "properties": { + "content": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "indicator": { + "ignore_above": 1024, + "type": "keyword" + }, + "title": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": "filebeat", + "rollover_alias": "filebeat-7.12.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } + } + } +} From 09b7f7c7075793ccf07868809ee793a3874c14d0 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 27 Jun 2021 23:33:43 -0500 Subject: [PATCH 19/38] Populate event enrichment call with actual alert fields This was previously hardcoded during development. --- .../security_solution/cti/index.ts | 8 +++++ .../cti_details/helpers.test.tsx | 34 ++++++++++++++++++- .../event_details/cti_details/helpers.tsx | 17 +++++++++- .../event_details/event_details.tsx | 12 +++---- .../cti/event_enrichment/translations.ts | 15 ++++++++ .../use_investigation_enrichment.ts | 14 ++++---- .../factory/cti/event_enrichment/helpers.ts | 13 ++++--- 7 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 79425f3c973b5..752eaf479f9a1 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -6,6 +6,7 @@ */ import { IEsSearchResponse } from 'src/plugins/data/public'; +import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants'; import { Inspect } from '../../common'; import { RequestBasicOptions } from '..'; @@ -18,9 +19,16 @@ export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { } export type CtiEnrichment = Record; +export type EventFields = Record; export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse { enrichments: CtiEnrichment[]; inspect: Inspect | null; totalCount: number; } + +export type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP; +export const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP) as EventField[]; + +export const isValidEventField = (field: string): field is EventField => + validEventFields.includes(field as EventField); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx index 7b6762c712d45..858962efa9e83 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx @@ -7,7 +7,11 @@ import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; -import { filterDuplicateEnrichments, parseExistingEnrichments } from './helpers'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, +} from './helpers'; describe('parseExistingEnrichments', () => { it('returns an empty array if data is empty', () => { @@ -443,3 +447,31 @@ describe('filterDuplicateEnrichments', () => { expect(filterDuplicateEnrichments(enrichments)).toEqual(enrichments); }); }); + +describe('getEnrichmentFields', () => { + it('returns an empty object if items is empty', () => { + expect(getEnrichmentFields([])).toEqual({}); + }); + + it('returns an object of event fields and values', () => { + const data = [ + { + category: 'source', + field: 'source.ip', + isObjectArray: false, + originalValue: ['192.168.1.1'], + values: ['192.168.1.1'], + }, + { + category: 'event', + field: 'event.reference', + isObjectArray: false, + originalValue: ['https://urlhaus.abuse.ch/url/1055419/'], + values: ['https://urlhaus.abuse.ch/url/1055419/'], + }, + ]; + expect(getEnrichmentFields(data)).toEqual({ + 'source.ip': '192.168.1.1', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 0314e4c55f6b8..b6694739f132d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -17,7 +17,11 @@ import { MATCHED_TYPE, } from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; -import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { + CtiEnrichment, + EventFields, + isValidEventField, +} from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; import * as i18n from './translations'; @@ -110,3 +114,14 @@ export const filterDuplicateEnrichments = (enrichments: CtiEnrichment[]): CtiEnr ) ?? enrichmentGroup[0] ); }; + +export const getEnrichmentFields = (items: TimelineEventsDetailsItem[]): EventFields => + items.reduce((fields, item) => { + if (isValidEventField(item.field)) { + const value = getFirstElement(item.originalValue); + if (value) { + return { ...fields, [item.field]: value }; + } + } + return fields; + }, {}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index b7ca455444daf..335e67e2b6fe0 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -28,6 +28,7 @@ import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/ti import { TimelineTabs } from '../../../../common/types/timeline'; import { filterDuplicateEnrichments, + getEnrichmentFields, parseExistingEnrichments, timelineDataToEnrichment, } from './cti_details/helpers'; @@ -105,11 +106,7 @@ const EventDetailsComponent: React.FC = ({ setSelectedTabId, ]); - const { - loading: enrichmentsLoading, - result: enrichmentsResponse, - } = useInvestigationTimeEnrichment(); - + const eventFields = useMemo(() => getEnrichmentFields(data ?? []), [data]); const existingEnrichments = useMemo( () => isAlert @@ -119,7 +116,10 @@ const EventDetailsComponent: React.FC = ({ : [], [data, isAlert] ); - + const { + loading: enrichmentsLoading, + result: enrichmentsResponse, + } = useInvestigationTimeEnrichment(eventFields); const allEnrichments = useMemo(() => { if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { return existingEnrichments; diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts new file mode 100644 index 0000000000000..ff9130b288fa8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.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 { i18n } from '@kbn/i18n'; + +export const INVESTIGATION_ENRICHMENT_REQUEST_ERROR = i18n.translate( + 'xpack.securitySolution.investigationEnrichment.requestError', + { + defaultMessage: `An error occurred while requesting threat intelligence`, + } +); diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index 0036d6711de36..164c3735bb451 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -8,15 +8,17 @@ import { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { EventFields } from '../../../../../common/search_strategy/security_solution/cti'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; -import { useEventEnrichment } from '.'; import { inputsActions } from '../../../store/actions'; +import * as i18n from './translations'; +import { useEventEnrichment } from '.'; export const QUERY_ID = 'investigation_time_enrichment'; const noop = () => {}; -export const useInvestigationTimeEnrichment = () => { +export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { const { addError } = useAppToasts(); const kibana = useKibana(); const dispatch = useDispatch(); @@ -48,7 +50,7 @@ export const useInvestigationTimeEnrichment = () => { useEffect(() => { if (error) { - addError(error, { title: 'TODO' }); + addError(error, { title: i18n.INVESTIGATION_ENRICHMENT_REQUEST_ERROR }); } }, [addError, error]); @@ -57,12 +59,10 @@ export const useInvestigationTimeEnrichment = () => { data: kibana.services.data, timerange: { from, to, interval: '' }, defaultIndex: ['filebeat-*'], // TODO do we apply the current sources here? - eventFields: { - 'source.ip': '192.168.1.19', // TODO get event values - }, + eventFields, filterQuery: '', // TODO do we apply the current filters here? }); - }, [from, start, kibana.services.data, to]); + }, [from, start, kibana.services.data, to, eventFields]); return { loading, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts index 9ab1114174ff4..f24bfc08b39e0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts @@ -12,13 +12,12 @@ import { ENRICHMENT_TYPES, EVENT_ENRICHMENT_INDICATOR_FIELD_MAP, } from '../../../../../../common/cti/constants'; -import { CtiEnrichment } from '../../../../../../common/search_strategy/security_solution/cti'; - -type EventField = keyof typeof EVENT_ENRICHMENT_INDICATOR_FIELD_MAP; -const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP) as EventField[]; - -const isValidEventField = (field: string): field is EventField => - validEventFields.includes(field as EventField); +import { + CtiEnrichment, + EventField, + isValidEventField, + validEventFields, +} from '../../../../../../common/search_strategy/security_solution/cti'; export const buildIndicatorShouldClauses = ( eventFields: Record From 7838a5cc7eece04b838d33b3e02d23aebe08b991 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Sun, 27 Jun 2021 23:48:17 -0500 Subject: [PATCH 20/38] Add a new field to our suspicious event to trigger enrichment The existing myhash field will generate an alert due to the way the rule is written, but the alert had no other fields that would match the investigation time enrichment. This gives it a source.ip, and updates the indicator to match. --- .../suspicious_source_event/data.json | 3 +++ .../es_archives/threat_indicator2/data.json | 19 +++---------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json index 11b5e9bd0828b..543250ba17499 100644 --- a/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/suspicious_source_event/data.json @@ -7,6 +7,9 @@ "@timestamp": "2021-02-22T21:00:49.337Z", "myhash": { "mysha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3" + }, + "source": { + "ip": "192.168.1.1" } } } diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json index 170691caa864d..62a6550943041 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json @@ -19,19 +19,9 @@ "threatintel": { "indicator": { "first_seen": "2021-03-11T08:02:14.000Z", - "file": { - "size": 80280, - "pe": {}, - "type": "elf", - "hash": { - "sha256": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", - "tlsh": "6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE", - "ssdeep": "1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL", - "md5": "9b6c3518a91d23ed77504b5416bfb5b3" - } - }, + "ip": "192.168.1.1", "provider": "another_provider", - "type": "file" + "type": "ip" }, "abusemalware": { "virustotal": { @@ -41,10 +31,7 @@ } } }, - "tags": [ - "threatintel-abusemalware", - "forwarded" - ], + "tags": ["threatintel-abusemalware", "forwarded"], "input": { "type": "httpjson" }, From ba91b57b51625a3e398ea4538ce66eb8782448f2 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 00:07:45 -0500 Subject: [PATCH 21/38] Only fetch enrichments data if there are valid event fields If none of the alert's fields would be relevant to the enrichment query, then we don't make the request at all. --- .../use_investigation_enrichment.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index 164c3735bb451..b0d0b99576613 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { isEmpty } from 'lodash'; import { EventFields } from '../../../../../common/search_strategy/security_solution/cti'; import { useAppToasts } from '../../../hooks/use_app_toasts'; @@ -55,13 +56,15 @@ export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { }, [addError, error]); useEffect(() => { - start({ - data: kibana.services.data, - timerange: { from, to, interval: '' }, - defaultIndex: ['filebeat-*'], // TODO do we apply the current sources here? - eventFields, - filterQuery: '', // TODO do we apply the current filters here? - }); + if (!isEmpty(eventFields)) { + start({ + data: kibana.services.data, + timerange: { from, to, interval: '' }, + defaultIndex: ['filebeat-*'], // TODO do we apply the current sources here? + eventFields, + filterQuery: '', // TODO do we apply the current filters here? + }); + } }, [from, start, kibana.services.data, to, eventFields]); return { From 14a09783dd1e6dd09119a7effe614405f2475535 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 11:56:52 -0500 Subject: [PATCH 22/38] Update enrichments matched.typed in integration tests This field was updated to reflect the source of the match, in this case: indicator match rules. --- .../indicator_match_rule.spec.ts | 4 ++-- .../tests/create_threat_matching.ts | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 27430ce9c8444..c627055235143 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -502,13 +502,13 @@ describe('indicator match', () => { cy.get(PROVIDER_BADGE).should('have.length', 3); cy.get(PROVIDER_BADGE).should( 'have.text', - `threat.indicator.matched.atomic: "${newThreatIndicatorRule.atomic}"threat.indicator.matched.type: "${newThreatIndicatorRule.type}"threat.indicator.matched.field: "${newThreatIndicatorRule.indicatorMappingField}"` + `threat.indicator.matched.atomic: "${newThreatIndicatorRule.atomic}"threat.indicator.matched.type: "indicator_match_rule"threat.indicator.matched.field: "${newThreatIndicatorRule.indicatorMappingField}"` ); cy.readFile(threatIndicatorPath).then((threatIndicator) => { cy.get(INDICATOR_MATCH_ROW_RENDER).should( 'have.text', - `threat.indicator.matched.field${newThreatIndicatorRule.indicatorMappingField}${accessibilityText}matched${newThreatIndicatorRule.indicatorMappingField}${newThreatIndicatorRule.atomic}${accessibilityText}threat.indicator.matched.type${newThreatIndicatorRule.type}${accessibilityText}fromthreat.indicator.event.dataset${threatIndicator.value.source.event.dataset}${accessibilityText}:threat.indicator.event.reference${threatIndicator.value.source.event.reference}(opens in a new tab or window)${accessibilityText}` + `threat.indicator.matched.field${newThreatIndicatorRule.indicatorMappingField}${accessibilityText}matched${newThreatIndicatorRule.indicatorMappingField}${newThreatIndicatorRule.atomic}${accessibilityText}threat.indicator.matched.typeindicator_match_rule${accessibilityText}fromthreat.indicator.event.dataset${threatIndicator.value.source.event.dataset}${accessibilityText}:threat.indicator.event.reference${threatIndicator.value.source.event.reference}(opens in a new tab or window)${accessibilityText}` ); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index e6a835462619c..c64713575c130 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -28,6 +28,7 @@ import { import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; import { SIGNALS_TEMPLATE_VERSION } from '../../../../plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template'; +import { ENRICHMENT_TYPES } from '../../../../plugins/security_solution/common/cti/constants'; const format = (value: unknown): string => JSON.stringify(value, null, 2); @@ -425,7 +426,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -457,7 +458,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -519,7 +520,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -544,7 +545,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978787', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'other_provider', type: 'ip', @@ -619,7 +620,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -649,7 +650,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.port', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -674,7 +675,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978787', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'other_provider', type: 'ip', @@ -754,7 +755,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -785,7 +786,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978783', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'destination.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, provider: 'geenensp', type: 'url', @@ -813,7 +814,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.ip', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', @@ -838,7 +839,7 @@ export default ({ getService }: FtrProviderContext) => { id: '978785', index: 'filebeat-8.0.0-2021.01.26-000001', field: 'source.port', - type: 'url', + type: ENRICHMENT_TYPES.IndicatorMatchRule, }, port: 57324, provider: 'geenensp', From f48ffe81650bbcd9eec4062208d700858eff5dbb Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 16:37:24 -0500 Subject: [PATCH 23/38] Ensure draggable fields are unique in a multi-match scenario If a given field matched multiple indicators, then the previous contextId was not unique as it was based on field/value that matched. Adding provider to the mix would fix it, except that we're not guaranteed to have a provider. I've added both provider (if present) and an index value to the key to ensure that it's unique. --- .../cti_details/threat_summary_view.tsx | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index 74d6e7d80b812..837d3b0fe8ed4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -31,6 +31,7 @@ export interface ThreatSummaryItem { timelineId: string; eventId: string; fieldName: string | undefined; + index: number; value: string | undefined; provider: string | undefined; }; @@ -62,42 +63,46 @@ const EnrichmentDescription: React.FC = ({ timelineId, eventId, fieldName, + index, value, provider, -}) => ( - - - - - {provider && ( - <> - - - {i18n.PROVIDER_PREPOSITION} - - - - - {provider} - - - - )} - -); +}) => { + const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${fieldName}-${value}-${index}-${provider}`; + return ( + + + + + {provider && ( + <> + + + {i18n.PROVIDER_PREPOSITION} + + + + + {provider} + + + + )} + + ); +}; const buildThreatSummaryItems = ( enrichments: CtiEnrichment[], timelineId: string, eventId: string ) => { - return enrichments.map((enrichment) => { + return enrichments.map((enrichment, index) => { const field = getEnrichmentValue(enrichment, MATCHED_FIELD); const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); const type = getEnrichmentValue(enrichment, MATCHED_TYPE); @@ -111,6 +116,7 @@ const buildThreatSummaryItems = ( description: { eventId, fieldName: field, + index, provider, timelineId, value, From accb6294301fe5d5a29d8612340dfb4d9c3ee280 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 16:50:56 -0500 Subject: [PATCH 24/38] Simplify types This field can never be null, as we always set it in our response. --- .../common/search_strategy/security_solution/cti/index.ts | 2 +- .../cti/event_enrichment/use_investigation_enrichment.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 752eaf479f9a1..da6f4abef7109 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -23,7 +23,7 @@ export type EventFields = Record; export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse { enrichments: CtiEnrichment[]; - inspect: Inspect | null; + inspect: Inspect; totalCount: number; } diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index b0d0b99576613..05cddbede7ef0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -37,7 +37,7 @@ export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { inputId: 'global', id: QUERY_ID, inspect: { - dsl: result.inspect?.dsl ?? [], + dsl: result.inspect.dsl, response: [JSON.stringify(result.rawResponse, null, 2)], }, loading, From 5a94f99132344d51093a4862951d7de058e1c826 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 16:52:49 -0500 Subject: [PATCH 25/38] Move helper functioons out of shared location and into consuming component These are unlikely to be used elsewhere. --- .../event_details/cti_details/enrichment_icon.tsx | 13 ++++++++++++- .../event_details/cti_details/helpers.tsx | 11 ----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx index 6a2d453759d37..042940a1cf036 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx @@ -8,7 +8,18 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; -import { getTooltipTitle, getTooltipContent } from './helpers'; +import * as i18n from './translations'; +import { isInvestigationTimeEnrichment } from './helpers'; + +export const getTooltipTitle = (type: string | undefined) => + isInvestigationTimeEnrichment(type) + ? i18n.INVESTIGATION_TOOLTIP_TITLE + : i18n.INDICATOR_TOOLTIP_TITLE; + +export const getTooltipContent = (type: string | undefined) => + isInvestigationTimeEnrichment(type) + ? i18n.INVESTIGATION_TOOLTIP_CONTENT + : i18n.INDICATOR_TOOLTIP_CONTENT; export const EnrichmentIcon: React.FC<{ type: string | undefined }> = ({ type }) => { return ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index b6694739f132d..1ff799af4d498 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -23,7 +23,6 @@ import { isValidEventField, } from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; -import * as i18n from './translations'; const isEventDetailsItem = ( item: TimelineEventsDetailsItem[] | null @@ -63,16 +62,6 @@ export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): Cti return acc; }, {}); -export const getTooltipTitle = (type: string | undefined) => - isInvestigationTimeEnrichment(type) - ? i18n.INVESTIGATION_TOOLTIP_TITLE - : i18n.INDICATOR_TOOLTIP_TITLE; - -export const getTooltipContent = (type: string | undefined) => - isInvestigationTimeEnrichment(type) - ? i18n.INVESTIGATION_TOOLTIP_CONTENT - : i18n.INDICATOR_TOOLTIP_CONTENT; - export const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => array ? array[0] : undefined; From 18c96fa92284e1523a35adc8bab6422fc06d5e5a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 17:12:55 -0500 Subject: [PATCH 26/38] Clean up data parsing logic using reduce This obviates the need for our filter/guard function and the extra loop that it entails. We have to specify the return value of our reduce fn, however, but that's mostly equivalent to our type guard. --- .../event_details/cti_details/helpers.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 1ff799af4d498..43c1dc54a39d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -24,10 +24,6 @@ import { } from '../../../../../common/search_strategy/security_solution/cti'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; -const isEventDetailsItem = ( - item: TimelineEventsDetailsItem[] | null -): item is TimelineEventsDetailsItem[] => !!item; - export const isInvestigationTimeEnrichment = (type: string | undefined) => type === ENRICHMENT_TYPES.InvestigationTime; @@ -44,16 +40,18 @@ export const parseExistingEnrichments = ( const { originalValue } = threatIndicatorField; const enrichmentStrings = Array.isArray(originalValue) ? originalValue : [originalValue]; - return enrichmentStrings - .map((enrichmentString) => { + return enrichmentStrings.reduce( + (enrichments, enrichmentString) => { try { - const enrichment = JSON.parse(enrichmentString); - return getDataFromSourceHits(enrichment); + const enrichment = getDataFromSourceHits(JSON.parse(enrichmentString)); + enrichments.push(enrichment); } catch (e) { - return null; + // omit failed parse } - }) - .filter(isEventDetailsItem); + return enrichments; + }, + [] + ); }; export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): CtiEnrichment => From 0196d72e238de2f32a4b0ab32f8288790a3d4c89 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 17:25:46 -0500 Subject: [PATCH 27/38] Move our general function into a general location --- .../common/utils/data_retrieval.test.ts | 26 +++++++++++++++++++ .../common/utils/data_retrieval.ts | 15 +++++++++++ .../event_details/cti_details/helpers.tsx | 4 +-- .../cti_details/threat_details_view.tsx | 2 +- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts create mode 100644 x-pack/plugins/security_solution/common/utils/data_retrieval.ts diff --git a/x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts b/x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts new file mode 100644 index 0000000000000..d7ab3986a14f9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/data_retrieval.test.ts @@ -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 { getFirstElement } from './data_retrieval'; + +describe('getFirstElement', () => { + it('returns undefined if array is undefined', () => { + expect(getFirstElement(undefined)).toEqual(undefined); + }); + + it('returns undefined if array is empty', () => { + expect(getFirstElement([])).toEqual(undefined); + }); + + it('returns the first element if present', () => { + expect(getFirstElement(['hi mom'])).toEqual('hi mom'); + }); + + it('returns the first element of multiple', () => { + expect(getFirstElement(['hi mom', 'hello world'])).toEqual('hi mom'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/utils/data_retrieval.ts b/x-pack/plugins/security_solution/common/utils/data_retrieval.ts new file mode 100644 index 0000000000000..04b6839b854b4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/data_retrieval.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. + */ + +/** + * Retrieves the first element of the given array. + * + * @param array the array to retrieve a value from + * @returns the first element of the array, or undefined if the array is undefined + */ +export const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => + array ? array[0] : undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 43c1dc54a39d8..91674852aabe8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -22,6 +22,7 @@ import { EventFields, isValidEventField, } from '../../../../../common/search_strategy/security_solution/cti'; +import { getFirstElement } from '../../../../../common/utils/data_retrieval'; import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; export const isInvestigationTimeEnrichment = (type: string | undefined) => @@ -60,9 +61,6 @@ export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): Cti return acc; }, {}); -export const getFirstElement: (array: T[] | undefined) => T | undefined = (array) => - array ? array[0] : undefined; - export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => getFirstElement(enrichment[field]) as string | undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index e21e6a5c2e92f..47bf182ec2160 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -32,11 +32,11 @@ import { PROVIDER, } from '../../../../../common/cti/constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; +import { getFirstElement } from '../../../../../common/utils/data_retrieval'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { getShimmedIndicatorValue, getEnrichmentValue, - getFirstElement, isInvestigationTimeEnrichment, } from './helpers'; import * as i18n from './translations'; From fabefb778d5c176157fd7a1626bbe8247f1c512d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 18:10:33 -0500 Subject: [PATCH 28/38] Extract the concept of "enrichment identifiers" This was already partially codified with 'buildEnrichmentId,' which is used to dedup enrichments; this extends the idea to all fields that could uniquely identify a given indicator. --- .../security_solution/cti/index.ts | 8 +++++++ .../event_details/cti_details/helpers.tsx | 17 ++++++++++++--- .../cti_details/threat_details_view.tsx | 21 ++++--------------- .../cti_details/threat_summary_view.tsx | 13 ++---------- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index da6f4abef7109..69a6841c7c14f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -21,6 +21,14 @@ export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { export type CtiEnrichment = Record; export type EventFields = Record; +export interface CtiEnrichmentIdentifiers { + id: string | undefined; + field: string | undefined; + value: string | undefined; + type: string | undefined; + provider: string | undefined; +} + export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse { enrichments: CtiEnrichment[]; inspect: Inspect; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 91674852aabe8..b048bb076e2d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -12,13 +12,16 @@ import { } from '../../../../../common/constants'; import { ENRICHMENT_TYPES, + MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_ID, MATCHED_TYPE, + PROVIDER, } from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { CtiEnrichment, + CtiEnrichmentIdentifiers, EventFields, isValidEventField, } from '../../../../../common/search_strategy/security_solution/cti'; @@ -74,11 +77,19 @@ export const getShimmedIndicatorValue = (enrichment: CtiEnrichment, field: strin getEnrichmentValue(enrichment, field) || getEnrichmentValue(enrichment, `${DEFAULT_INDICATOR_SOURCE_PATH}.${field}`); +export const getEnrichmentIdentifiers = (enrichment: CtiEnrichment): CtiEnrichmentIdentifiers => ({ + id: getEnrichmentValue(enrichment, MATCHED_ID), + field: getEnrichmentValue(enrichment, MATCHED_FIELD), + value: getEnrichmentValue(enrichment, MATCHED_ATOMIC), + type: getEnrichmentValue(enrichment, MATCHED_TYPE), + provider: getShimmedIndicatorValue(enrichment, PROVIDER), +}); + const buildEnrichmentId = (enrichment: CtiEnrichment): string => { - const matchedId = getEnrichmentValue(enrichment, MATCHED_ID); - const matchedField = getEnrichmentValue(enrichment, MATCHED_FIELD); - return `${matchedId}${matchedField}`; + const { id, field } = getEnrichmentIdentifiers(enrichment); + return `${id}${field}`; }; + /** * This function receives an array of enrichments and removes * investigation-time enrichments if that exact indicator already exists diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index 47bf182ec2160..d5e985c5757a6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -21,23 +21,14 @@ import React, { Fragment } from 'react'; import { StyledEuiInMemoryTable } from '../summary_view'; import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from '../helpers'; import { EmptyThreatDetailsView } from './empty_threat_details_view'; -import { - FIRSTSEEN, - EVENT_URL, - EVENT_REFERENCE, - MATCHED_ID, - MATCHED_FIELD, - MATCHED_ATOMIC, - MATCHED_TYPE, - PROVIDER, -} from '../../../../../common/cti/constants'; +import { FIRSTSEEN, EVENT_URL, EVENT_REFERENCE } from '../../../../../common/cti/constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { getFirstElement } from '../../../../../common/utils/data_retrieval'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import { getShimmedIndicatorValue, - getEnrichmentValue, isInvestigationTimeEnrichment, + getEnrichmentIdentifiers, } from './helpers'; import * as i18n from './translations'; import { EnrichmentIcon } from './enrichment_icon'; @@ -150,14 +141,10 @@ const ThreatDetailsViewComponent: React.FC<{ <> {sortedEnrichments.map((enrichment, index) => { - const key = getEnrichmentValue(enrichment, MATCHED_ID); - const field = getEnrichmentValue(enrichment, MATCHED_FIELD); - const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); - const type = getEnrichmentValue(enrichment, MATCHED_TYPE); - const provider = getShimmedIndicatorValue(enrichment, PROVIDER); + const { id, field, provider, type, value } = getEnrichmentIdentifiers(enrichment); return ( - + { return enrichments.map((enrichment, index) => { - const field = getEnrichmentValue(enrichment, MATCHED_FIELD); - const value = getEnrichmentValue(enrichment, MATCHED_ATOMIC); - const type = getEnrichmentValue(enrichment, MATCHED_TYPE); - const provider = getShimmedIndicatorValue(enrichment, PROVIDER); + const { field, type, value, provider } = getEnrichmentIdentifiers(enrichment); return { title: { From 81dd9277bb8f236fa64e66dca4a847107650f808 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 18:19:59 -0500 Subject: [PATCH 29/38] Use existing constant as the source of our enrichments query This is now used by both the overview card and the enrichment query. --- .../cti/event_enrichment/use_investigation_enrichment.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index 05cddbede7ef0..e6264235be4c5 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -15,6 +15,7 @@ import { useKibana } from '../../../lib/kibana'; import { inputsActions } from '../../../store/actions'; import * as i18n from './translations'; import { useEventEnrichment } from '.'; +import { DEFAULT_CTI_SOURCE_INDEX } from '../../../../../common/cti/constants'; export const QUERY_ID = 'investigation_time_enrichment'; const noop = () => {}; @@ -60,9 +61,9 @@ export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { start({ data: kibana.services.data, timerange: { from, to, interval: '' }, - defaultIndex: ['filebeat-*'], // TODO do we apply the current sources here? + defaultIndex: DEFAULT_CTI_SOURCE_INDEX, eventFields, - filterQuery: '', // TODO do we apply the current filters here? + filterQuery: '', }); } }, [from, start, kibana.services.data, to, eventFields]); From 48d3f6beaaf1e1fb4c9502e9ae756f7964d1715a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 18:25:36 -0500 Subject: [PATCH 30/38] Codify our default enrichment lookback as constants --- .../plugins/security_solution/common/cti/constants.ts | 3 +++ .../event_enrichment/use_investigation_enrichment.ts | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index b1f1ac7a58883..631a13df1ecb1 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -57,6 +57,9 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { 'registry.path': 'threatintel.indicator.registry.path', }; +export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; +export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; + export const CTI_DEFAULT_SOURCES = [ 'Abuse URL', 'Abuse Malware', diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index e6264235be4c5..75d0f4165eb64 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -10,12 +10,16 @@ import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash'; import { EventFields } from '../../../../../common/search_strategy/security_solution/cti'; +import { + DEFAULT_CTI_SOURCE_INDEX, + DEFAULT_EVENT_ENRICHMENT_FROM, + DEFAULT_EVENT_ENRICHMENT_TO, +} from '../../../../../common/cti/constants'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; import { inputsActions } from '../../../store/actions'; import * as i18n from './translations'; import { useEventEnrichment } from '.'; -import { DEFAULT_CTI_SOURCE_INDEX } from '../../../../../common/cti/constants'; export const QUERY_ID = 'investigation_time_enrichment'; const noop = () => {}; @@ -24,7 +28,10 @@ export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { const { addError } = useAppToasts(); const kibana = useKibana(); const dispatch = useDispatch(); - const [{ from, to }, setRange] = useState({ from: 'now-30d', to: 'now' }); + const [{ from, to }, setRange] = useState({ + from: DEFAULT_EVENT_ENRICHMENT_FROM, + to: DEFAULT_EVENT_ENRICHMENT_TO, + }); const { error, loading, result, start } = useEventEnrichment(); const deleteQuery = useCallback(() => { From 866cb27a74d8af9a76d69605011db99a4c44c92f Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 19:18:04 -0500 Subject: [PATCH 31/38] Remove unnecessary flexbox The generic SummaryView component previously had to deal with multi-valued CTI fields, representing the multiple values coming from the multiple nested objects with that field. However, with the new UI we no longer have that constraint, and so the default columnar style, and the corresponding overriding styles, are no longer necessary. --- .../cti_details/threat_summary_view.tsx | 15 ++++----------- .../components/event_details/summary_view.tsx | 7 ------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index ae70bdcdb0701..4a6c9ec48bcbc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -31,26 +31,19 @@ export interface ThreatSummaryItem { }; } -// Overrides flex styles declared in StyledEuiInMemoryTable -const FlexContainer = styled.div` - display: flex; - align-items: center; - width: 100%; -`; - const RightMargin = styled.span` margin-right: ${({ theme }) => theme.eui.paddingSizes.s}; `; const EnrichmentTitle: React.FC = ({ title, type }) => ( - + <>
{title}
-
+ ); const EnrichmentDescription: React.FC = ({ @@ -63,7 +56,7 @@ const EnrichmentDescription: React.FC = ({ }) => { const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${fieldName}-${value}-${index}-${provider}`; return ( - + <> = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 6d4271c42dffd..0e846f3f6f699 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -19,13 +19,6 @@ export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` .euiTableRowCell { border: none; } - - /* TODO is this needed? */ - .euiTableCellContent { - display: flex; - flex-direction: column; - align-items: flex-start; - } `; const StyledEuiTitle = styled(EuiTitle)` From f1e843c5e7669179ce428ad9591651f39673d85d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 19:54:37 -0500 Subject: [PATCH 32/38] Filter out partial responses in the event enrichment observable The UI does not currently handle these. We need to test the behavior of long-running queries with this filter, but this should simplify the behavior to complete/error until we handle partial responses. --- .../containers/cti/event_enrichment/api.ts | 22 +++++++++++++++---- .../event_enrichment/use_event_enrichment.ts | 8 +++++-- .../use_investigation_enrichment.ts | 4 ++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts index 245cea6a1e21a..179b4a53e3676 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts @@ -6,14 +6,24 @@ */ import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { + isErrorResponse, + isCompleteResponse, +} from '../../../../../../../../src/plugins/data/common'; import { CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, } from '../../../../../common/search_strategy/security_solution/cti'; +type GetEventEnrichmentProps = CtiEventEnrichmentRequestOptions & { + data: DataPublicPluginStart; + signal: AbortSignal; +}; + export const getEventEnrichment = ({ data, defaultIndex, @@ -21,10 +31,7 @@ export const getEventEnrichment = ({ filterQuery, timerange, signal, -}: CtiEventEnrichmentRequestOptions & { - data: DataPublicPluginStart; - signal: AbortSignal; -}): Observable => +}: GetEventEnrichmentProps): Observable => data.search.search( { defaultIndex, @@ -38,3 +45,10 @@ export const getEventEnrichment = ({ abortSignal: signal, } ); + +export const getEventEnrichmentComplete = ( + props: GetEventEnrichmentProps +): Observable => + getEventEnrichment(props).pipe( + filter((response) => isErrorResponse(response) || isCompleteResponse(response)) + ); diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts index ddc9c27f68c7a..939566d6e59c3 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts @@ -7,9 +7,13 @@ import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; -import { getEventEnrichment } from './api'; +import { getEventEnrichment, getEventEnrichmentComplete } from './api'; -// TODO define filtered version of getEventEnrichment that excludes incomplete responses const getEventEnrichmentOptionalSignal = withOptionalSignal(getEventEnrichment); export const useEventEnrichment = () => useObservable(getEventEnrichmentOptionalSignal); + +const getEventEnrichmentCompleteWithOptionalSignal = withOptionalSignal(getEventEnrichmentComplete); + +export const useEventEnrichmentComplete = () => + useObservable(getEventEnrichmentCompleteWithOptionalSignal); diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index 75d0f4165eb64..c15b49fe5c41e 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -19,7 +19,7 @@ import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; import { inputsActions } from '../../../store/actions'; import * as i18n from './translations'; -import { useEventEnrichment } from '.'; +import { useEventEnrichmentComplete } from '.'; export const QUERY_ID = 'investigation_time_enrichment'; const noop = () => {}; @@ -32,7 +32,7 @@ export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { from: DEFAULT_EVENT_ENRICHMENT_FROM, to: DEFAULT_EVENT_ENRICHMENT_TO, }); - const { error, loading, result, start } = useEventEnrichment(); + const { error, loading, result, start } = useEventEnrichmentComplete(); const deleteQuery = useCallback(() => { dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: QUERY_ID })); From d2aa4cde2df22942fda023fe518bc94fb9e5c616 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 21:59:36 -0500 Subject: [PATCH 33/38] Display placeholders while event enrichment is loading Displays a loading spinner in the Threat Intel tab title, and some loading lines where the enrichments summary is. --- .../components/event_details/event_details.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 335e67e2b6fe0..19b044a214349 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -12,6 +12,8 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiLoadingContent, + EuiLoadingSpinner, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -145,6 +147,11 @@ const EventDetailsComponent: React.FC = ({ title: i18n.ALERT_SUMMARY, }} /> + {enrichmentsLoading && ( + <> + + + )} {enrichmentCount > 0 && ( <> = ({ id, browserFields, timelineId, + enrichmentsLoading, enrichmentCount, allEnrichments, viewThreatIntelTab, @@ -182,11 +190,16 @@ const EventDetailsComponent: React.FC = ({ ? { id: EventsViewType.threatIntelView, 'data-test-subj': 'threatIntelTab', - name: `${i18n.THREAT_INTEL} (${enrichmentCount})`, + name: ( + + {`${i18n.THREAT_INTEL} `} + {enrichmentsLoading ? : `(${enrichmentCount})`} + + ), content: , } : undefined, - [allEnrichments, enrichmentCount, isAlert] + [allEnrichments, enrichmentCount, enrichmentsLoading, isAlert] ); const tableTab = useMemo( From 811174c3be978569a2a02510fee8c7a6b3720b29 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 28 Jun 2021 23:19:37 -0500 Subject: [PATCH 34/38] Update our indicator data to be within the last 30 days This fixes our cypress test, but it's going to start failing again in 30 days. However, by that time I'll have implemented the absolute data picker, which will allow for a more comprehensive test in addition to us sidestepping this issue. --- .../integration/detection_alerts/cti_enrichments.spec.ts | 4 ++-- .../es_archives/threat_indicator2/data.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index 5aadc1c279b08..c4bbce89f2c8a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -170,8 +170,8 @@ describe('CTI Enrichment', () => { value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', }; const investigationTimeEnrichment = { - field: 'myhash.mysha256', - value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + field: 'source.ip', + value: '192.168.1.1', }; const expectedMatches = [indicatorMatchRuleEnrichment, investigationTimeEnrichment]; diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json index 62a6550943041..0598fd7ba7c86 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json @@ -4,7 +4,7 @@ "id": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", "index": "filebeat-7.12.0-2021.03.11-000001", "source": { - "@timestamp": "2021-03-11T14:51:05.766Z", + "@timestamp": "2021-06-27T14:51:05.766Z", "agent": { "ephemeral_id": "34c78500-8df5-4a07-ba87-1cc738b98431", "hostname": "test", From 3fdd33b4d312cdaa0f1d21407ff4de9a87909745 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 29 Jun 2021 08:28:23 -0500 Subject: [PATCH 35/38] Fix type error with our details tabs The name prop on a Tab will be rendered as a node, so both strings and elements are acceptable. This relaxes the types to inherit from the component itself. --- .../common/components/event_details/event_details.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 19b044a214349..d07cdd81aa5f4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -35,11 +35,7 @@ import { timelineDataToEnrichment, } from './cti_details/helpers'; -interface EventViewTab { - id: EventViewId; - name: string; - content: JSX.Element; -} +type EventViewTab = EuiTabbedContentTab; export type EventViewId = | EventsViewType.tableView @@ -130,7 +126,7 @@ const EventDetailsComponent: React.FC = ({ }, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]); const enrichmentCount = allEnrichments.length; - const summaryTab = useMemo( + const summaryTab: EventViewTab | undefined = useMemo( () => isAlert ? { From 93811257b12224df89b3850eee7e20256d1684ca Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 29 Jun 2021 08:28:43 -0500 Subject: [PATCH 36/38] Fix failing jest tests The addition of our filtering of the search observable broke this test, since we now need to implement the search observable. Rather than do that, we'll instead mock our local hook as that's more likely to change. --- .../common/components/event_details/event_details.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 6aff259d8220e..f599cfa242dea 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -19,8 +19,10 @@ import { useMountAppended } from '../../utils/use_mount_appended'; import { mockAlertDetailsData } from './__mocks__'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TimelineTabs } from '../../../../common/types/timeline'; +import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; jest.mock('../../../common/lib/kibana'); +jest.mock('../../containers/cti/event_enrichment'); jest.mock('../link_to'); describe('EventDetails', () => { @@ -46,6 +48,7 @@ describe('EventDetails', () => { let wrapper: ReactWrapper; let alertsWrapper: ReactWrapper; beforeAll(async () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({}); wrapper = mount( From 8275e0541f558d1abff360f5d95dce456de8e926 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 29 Jun 2021 20:14:31 -0500 Subject: [PATCH 37/38] Skips flaky cypress test See details on https://github.com/elastic/kibana/issues/84020 --- .../cypress/integration/detection_rules/override.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index a791cc293c1f0..87a8c73f61b79 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -88,7 +88,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Detection rules, override', () => { +// https://github.com/elastic/kibana/issues/84020 +describe.skip('Detection rules, override', () => { const expectedUrls = newOverrideRule.referenceUrls.join(''); const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); const expectedTags = newOverrideRule.tags.join(''); From e0079726310c2bfff3dc1f3622a7bbd95423ab10 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 29 Jun 2021 21:19:02 -0500 Subject: [PATCH 38/38] Fix archive mappings to fix cypress test failure This override test is failing due to a mapping conflict, which causes the rule name override field to be invalid and not persisted. The mapping conflict is due to a typo in the threat_indicator2 archive, where the document declares one index but the mappings declare another. While es_archive happily loads the document into the index it specifies, the mappings already exist via the threat_indicator archive, and thus the new threat_indicator2 index receives dynamic mappings. Additionally, because the index created by threat_indicator2 is not the index specified in threat_indicator2's mappings, the behavior seems to be to leave that index when calling unload! Thus, on the next test that cares (override.spec.ts), we have an errant filebeat (threat_indicator2) index with default mappings, causing mapping conflicts with our other ECS indexes. --- .../cypress/integration/detection_rules/override.spec.ts | 3 +-- .../es_archives/threat_indicator2/mappings.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 87a8c73f61b79..a791cc293c1f0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -88,8 +88,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; -// https://github.com/elastic/kibana/issues/84020 -describe.skip('Detection rules, override', () => { +describe('Detection rules, override', () => { const expectedUrls = newOverrideRule.referenceUrls.join(''); const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); const expectedTags = newOverrideRule.tags.join(''); diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json index efd23c5a6bba4..072318f7f4fc4 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json @@ -3,10 +3,10 @@ "value": { "aliases": { "filebeat-7.12.0": { - "is_write_index": true + "is_write_index": false } }, - "index": "filebeat-7.12.0-2021.03.10-000001", + "index": "filebeat-7.12.0-2021.03.11-000001", "mappings": { "_meta": { "beat": "filebeat",