From 199d4c683d7f1ce8a54498211f874effe9522b98 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 30 Jun 2021 02:34:21 -0500 Subject: [PATCH] [Security Solution][CTI] Investigation time enrichment UI (#103383) (#103829) * 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. * 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. * Move test constants into tests that use them * style: declare FC function as an FC * Extract some inline parsing logic into a helper function And test it! * 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 * 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. * 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. * Sort our details fields This promotes sanity for the user. * Add "view threat intel data" button This simply opens the threat intel tab. * Implement header for threat details sections * Add a basic jest "unit" test around ThreatSummaryView * 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. * 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. * 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/ * 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 * 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. * 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. * Populate event enrichment call with actual alert fields This was previously hardcoded during development. * 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. * 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. * Update enrichments matched.typed in integration tests This field was updated to reflect the source of the match, in this case: indicator match rules. * 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. * Simplify types This field can never be null, as we always set it in our response. * Move helper functioons out of shared location and into consuming component These are unlikely to be used elsewhere. * 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. * Move our general function into a general location * 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. * Use existing constant as the source of our enrichments query This is now used by both the overview card and the enrichment query. * Codify our default enrichment lookback as constants * 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. * 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. * 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. * 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. * 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. * 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. # Conflicts: # x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx --- .../security_solution/common/cti/constants.ts | 18 +- .../security_solution/cti/index.mock.ts | 55 +- .../security_solution/cti/index.ts | 18 +- .../common/utils/data_retrieval.test.ts | 26 + .../common/utils/data_retrieval.ts | 15 + .../detection_alerts/cti_enrichments.spec.ts | 193 ++++ .../indicator_match_rule.spec.ts | 176 +--- .../cypress/screens/alerts_details.ts | 2 +- .../empty_threat_details_view.test.tsx | 10 +- .../empty_threat_details_view.tsx | 3 +- .../cti_details/enrichment_icon.tsx | 30 + .../cti_details/helpers.test.tsx | 477 ++++++++++ .../event_details/cti_details/helpers.tsx | 123 +++ .../cti_details/threat_details_view.test.tsx | 88 ++ .../cti_details/threat_details_view.tsx | 163 ++++ .../cti_details/threat_summary_view.test.tsx | 41 + .../cti_details/threat_summary_view.tsx | 141 +++ .../event_details/cti_details/translations.ts | 71 ++ .../event_details/event_details.test.tsx | 5 +- .../event_details/event_details.tsx | 114 ++- .../components/event_details/helpers.tsx | 13 +- .../components/event_details/summary_view.tsx | 8 +- .../threat_details_view.test.tsx | 114 --- .../event_details/threat_details_view.tsx | 122 --- .../threat_summary_view.test.tsx | 43 - .../event_details/threat_summary_view.tsx | 78 -- .../components/event_details/translations.ts | 19 +- .../containers/cti/event_enrichment/api.ts | 54 ++ .../containers/cti/event_enrichment/index.ts | 9 + .../cti/event_enrichment/translations.ts | 15 + .../event_enrichment/use_event_enrichment.ts | 19 + .../use_investigation_enrichment.ts | 83 ++ .../enrich_signal_threat_matches.test.ts | 27 +- .../enrich_signal_threat_matches.ts | 10 +- .../factory/cti/event_enrichment/helpers.ts | 19 +- .../tests/create_threat_matching.ts | 23 +- .../suspicious_source_event/data.json | 3 + .../es_archives/threat_indicator/data.json | 1 - .../es_archives/threat_indicator2/data.json | 63 ++ .../threat_indicator2/mappings.json | 822 ++++++++++++++++++ 40 files changed, 2663 insertions(+), 651 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 create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/empty_threat_details_view.test.tsx (84%) rename x-pack/plugins/security_solution/public/common/components/event_details/{ => cti_details}/empty_threat_details_view.tsx (96%) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx 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 create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx 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_details_view.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts 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/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 4e935f3e497f4..631a13df1ecb1 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}`; @@ -37,13 +39,10 @@ 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', +} export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { 'file.hash.md5': 'threatintel.indicator.file.hash.md5', @@ -58,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/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/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 788a44bc5b9f7..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 @@ -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,24 @@ 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; + inspect: Inspect; 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/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/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..b03daf74ce247 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -0,0 +1,193 @@ +/* + * 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 { 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 } from '../../tasks/alerts'; +import { createCustomIndicatorRule } from '../../tasks/api_calls/rules'; +import { + openJsonView, + openThreatIndicatorDetails, + scrollJsonViewToBottom, +} from '../../tasks/alerts_details'; + +import { ALERTS_URL } from '../../urls/navigation'; +import { addsFieldsToTimeline } from '../../tasks/rule_details'; + +describe('CTI Enrichment', () => { + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('suspicious_source_event'); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); + goToManageAlertsDetectionRules(); + createCustomIndicatorRule(newThreatIndicatorRule); + reload(); + }); + + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('suspicious_source_event'); + }); + + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(ALERTS_URL); + goToManageAlertsDetectionRules(); + goToRuleDetails(); + }); + + it('Displays enrichment matched.* fields on the timeline', () => { + const expectedFields = { + 'threat.indicator.matched.atomic': newThreatIndicatorRule.atomic, + 'threat.indicator.matched.type': 'indicator_match_rule', + 'threat.indicator.matched.field': newThreatIndicatorRule.indicatorMappingField, + }; + const fields = Object.keys(expectedFields) as Array; + + addsFieldsToTimeline('threat.indicator.matched', fields); + + fields.forEach((field) => { + cy.get(TIMELINE_FIELD(field)).should('have.text', expectedFields[field]); + }); + }); + + 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\\":\\"indicator_match_rule\\"}}"', + }, + { line: 2, text: ' }' }, + ]; + + 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 indicator details on the threat intel tab', () => { + const expectedThreatIndicatorData = [ + { 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: '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.sha256', + value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + }, + { + field: 'file.hash.ssdeep', + value: '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', + }, + { + field: 'file.hash.tlsh', + value: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', + }, + { 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', + }, + { 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: 'indicator_match_rule' }, + { field: '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('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: 'source.ip', + value: '192.168.1.1', + }; + 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/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 c2e8a92474814..e1268c52f75d4 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 { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; @@ -520,170 +502,18 @@ 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}` ); }); }); }); - 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(ALERTS_URL); - goToManageAlertsDetectionRules(); - createCustomIndicatorRule(newThreatIndicatorRule); - reload(); - }); - - after(() => { - esArchiverUnload('threat_indicator'); - esArchiverUnload('suspicious_source_event'); - }); - - beforeEach(() => { - loginAndWaitForPageWithoutDateRange(ALERTS_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(); 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/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 84% 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..76c6b077236f0 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,11 +8,11 @@ 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'); +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/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/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..042940a1cf036 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx @@ -0,0 +1,30 @@ +/* + * 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 * 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.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx new file mode 100644 index 0000000000000..858962efa9e83 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx @@ -0,0 +1,477 @@ +/* + * 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 { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + 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'], + }, + ]), + ]); + }); +}); + +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); + }); +}); + +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 new file mode 100644 index 0000000000000..b048bb076e2d3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -0,0 +1,123 @@ +/* + * 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 { groupBy } from 'lodash'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + INDICATOR_DESTINATION_PATH, +} 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'; +import { getFirstElement } from '../../../../../common/utils/data_retrieval'; +import { getDataFromSourceHits } from '../../../../../common/utils/field_formatters'; + +export const isInvestigationTimeEnrichment = (type: string | undefined) => + type === ENRICHMENT_TYPES.InvestigationTime; + +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.reduce( + (enrichments, enrichmentString) => { + try { + const enrichment = getDataFromSourceHits(JSON.parse(enrichmentString)); + enrichments.push(enrichment); + } catch (e) { + // omit failed parse + } + return enrichments; + }, + [] + ); +}; + +export const timelineDataToEnrichment = (data: TimelineEventsDetailsItem[]): CtiEnrichment => + data.reduce((acc, item) => { + acc[item.field] = item.originalValue; + return acc; + }, {}); + +export const getEnrichmentValue = (enrichment: CtiEnrichment, field: string) => + getFirstElement(enrichment[field]) as string | undefined; + +/** + * 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 getShimmedIndicatorValue = (enrichment: CtiEnrichment, field: string) => + 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 { 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 + * 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) => !isInvestigationTimeEnrichment(getEnrichmentValue(enrichment, MATCHED_TYPE)) + ) ?? 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/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 new file mode 100644 index 0000000000000..0113dde96a4b6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { mount } from 'enzyme'; + +import { TestProviders } from '../../../mock'; +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', () => { + it('renders a detail view 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-details-view"]').hostNodes()).toHaveLength( + enrichments.length + ); + }); + + 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); + }); + + 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); + }); + + 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 enrichments = [existingEnrichment, newEnrichment]; + + 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 new file mode 100644 index 0000000000000..d5e985c5757a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -0,0 +1,163 @@ +/* + * 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, + EuiText, + EuiTextColor, +} 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 } 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, + isInvestigationTimeEnrichment, + getEnrichmentIdentifiers, +} 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); + const firstSeenDate = Date.parse(firstSeenValue ?? 'no date'); + 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} + + + + )} + + + + + + {isInvestigationTimeEnrichment(type) && ( + + + + + + )} + +); + +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) + .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]), + }, + }; + }); + +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 { id, field, provider, type, value } = getEnrichmentIdentifiers(enrichment); + + return ( + + + + {index < sortedEnrichments.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.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx new file mode 100644 index 0000000000000..bf6c4b9594344 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 { ThreatSummaryView } from './threat_summary_view'; +import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; + +jest.mock('../../../../timelines/components/timeline/body/renderers/formatted_field'); + +describe('ThreatSummaryView', () => { + const mount = useMountAppended(); + const eventId = '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'; + const timelineId = 'detections-page'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + 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"] .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 new file mode 100644 index 0000000000000..4a6c9ec48bcbc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -0,0 +1,141 @@ +/* + * 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, 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 { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { getEnrichmentIdentifiers } from './helpers'; +import { EnrichmentIcon } from './enrichment_icon'; + +export interface ThreatSummaryItem { + title: { + title: string | undefined; + type: string | undefined; + }; + description: { + timelineId: string; + eventId: string; + fieldName: string | undefined; + index: number; + value: string | undefined; + provider: string | undefined; + }; +} + +const RightMargin = styled.span` + margin-right: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const EnrichmentTitle: React.FC = ({ title, type }) => ( + <> + + + + +
{title}
+
+ +); + +const EnrichmentDescription: React.FC = ({ + timelineId, + eventId, + fieldName, + index, + value, + 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, index) => { + const { field, type, value, provider } = getEnrichmentIdentifiers(enrichment); + + return { + title: { + title: field, + type, + }, + description: { + eventId, + fieldName: field, + index, + 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..a0c247db927ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -0,0 +1,71 @@ +/* + * 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.', + } +); + +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_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.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 82d0162228823..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 @@ -5,6 +5,7 @@ * 2.0. */ +import { waitFor } from '@testing-library/dom'; import { ReactWrapper } from 'enzyme'; import React from 'react'; @@ -18,9 +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 { waitFor } from '@testing-library/dom'; +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( 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..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 @@ -5,27 +5,37 @@ * 2.0. */ -import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; +import { + EuiTabbedContent, + EuiTabbedContentTab, + EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiLoadingSpinner, +} from '@elastic/eui'; import React, { useCallback, 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 { ThreatDetailsView } from './threat_details_view'; +import { ThreatSummaryView } from './cti_details/threat_summary_view'; +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 { INDICATOR_DESTINATION_PATH } from '../../../../common/constants'; -import { getDataFromSourceHits } from '../../../../common/utils/field_formatters'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, + timelineDataToEnrichment, +} from './cti_details/helpers'; -interface EventViewTab { - id: EventViewId; - name: string; - content: JSX.Element; -} +type EventViewTab = EuiTabbedContentTab; export type EventViewId = | EventsViewType.tableView @@ -90,23 +100,33 @@ const EventDetailsComponent: React.FC = ({ (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), [setSelectedTabId] ); + const viewThreatIntelTab = useCallback(() => setSelectedTabId(EventsViewType.threatIntelView), [ + 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))); + const eventFields = useMemo(() => getEnrichmentFields(data ?? []), [data]); + const existingEnrichments = useMemo( + () => + isAlert + ? parseExistingEnrichments(data).map((enrichmentData) => + timelineDataToEnrichment(enrichmentData) + ) + : [], + [data, isAlert] + ); + const { + loading: enrichmentsLoading, + result: enrichmentsResponse, + } = useInvestigationTimeEnrichment(eventFields); + const allEnrichments = useMemo(() => { + if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { + return existingEnrichments; } - return []; - }, [data, isAlert]); - - const threatCount = useMemo(() => threatData.length, [threatData.length]); + return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]); + }, [enrichmentsLoading, enrichmentsResponse, existingEnrichments]); + const enrichmentCount = allEnrichments.length; - const summaryTab = useMemo( + const summaryTab: EventViewTab | undefined = useMemo( () => isAlert ? { @@ -120,15 +140,44 @@ const EventDetailsComponent: React.FC = ({ eventId: id, browserFields, timelineId, - title: threatCount ? i18n.ALERT_SUMMARY : undefined, + title: i18n.ALERT_SUMMARY, }} /> - {threatCount > 0 && } + {enrichmentsLoading && ( + <> + + + )} + {enrichmentCount > 0 && ( + <> + + + + + {i18n.VIEW_CTI_DATA} + + + + )} ), } : undefined, - [browserFields, data, id, isAlert, timelineId, threatCount] + [ + isAlert, + data, + id, + browserFields, + timelineId, + enrichmentsLoading, + enrichmentCount, + allEnrichments, + viewThreatIntelTab, + ] ); const threatIntelTab = useMemo( @@ -137,11 +186,16 @@ const EventDetailsComponent: React.FC = ({ ? { id: EventsViewType.threatIntelView, 'data-test-subj': 'threatIntelTab', - name: `${i18n.THREAT_INTEL} (${threatCount})`, - content: , + name: ( + + {`${i18n.THREAT_INTEL} `} + {enrichmentsLoading ? : `(${enrichmentCount})`} + + ), + content: , } : undefined, - [isAlert, threatCount, threatData] + [allEnrichments, enrichmentCount, enrichmentsLoading, 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..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 @@ -12,19 +12,13 @@ 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; } .euiTableRowCell { border: none; } - - .euiTableCellContent { - display: flex; - flex-direction: column; - align-items: flex-start; - } `; const StyledEuiTitle = styled(EuiTitle)` 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/threat_details_view.test.tsx deleted file mode 100644 index 4b2f56a205042..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_details_view.test.tsx +++ /dev/null @@ -1,114 +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 React from 'react'; - -import { ThreatDetailsView } from './threat_details_view'; - -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'], - }, - ], -]; - -describe('ThreatDetailsView', () => { - const mount = useMountAppended(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('render correct items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="threat-details-view-0"]').exists()).toEqual(true); - }); - - test('renders empty view if there are no items', () => { - 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( - - - - ); - 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); - }); -}); 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/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx deleted file mode 100644 index fa12ff3db7895..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/threat_summary_view.test.tsx +++ /dev/null @@ -1,43 +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 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'; - -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', -}; - -describe('ThreatSummaryView', () => { - const mount = useMountAppended(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('render correct items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="threat-summary-view"]').exists()).toEqual(true); - }); -}); 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 67b09e8e59699..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 getDescription = ({ - contextId, - eventId, - fieldName, - values, -}: ThreatSummaryRow['description']): JSX.Element => ( - <> - {values.map((value: string) => ( - - ))} - -); - -const summaryColumns: Array> = getSummaryColumns(getDescription); - -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/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index a28d1976ca940..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,23 +23,8 @@ 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 VIEW_CTI_DATA = i18n.translate('xpack.securitySolution.alertDetails.threatIntelCta', { + defaultMessage: 'View threat intel data', }); export const INVESTIGATION_GUIDE = i18n.translate( 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 new file mode 100644 index 0000000000000..179b4a53e3676 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/api.ts @@ -0,0 +1,54 @@ +/* + * 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 { 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, + eventFields, + filterQuery, + timerange, + signal, +}: GetEventEnrichmentProps): Observable => + data.search.search( + { + defaultIndex, + eventFields, + factoryQueryType: CtiQueries.eventEnrichment, + filterQuery, + timerange, + }, + { + strategy: 'securitySolutionSearchStrategy', + 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/index.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts new file mode 100644 index 0000000000000..e8fb1a03045d9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './use_event_enrichment'; +export * from './use_investigation_enrichment'; 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_event_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts new file mode 100644 index 0000000000000..939566d6e59c3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_event_enrichment.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; + +import { getEventEnrichment, getEventEnrichmentComplete } from './api'; + +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 new file mode 100644 index 0000000000000..c15b49fe5c41e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +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 { useEventEnrichmentComplete } from '.'; + +export const QUERY_ID = 'investigation_time_enrichment'; +const noop = () => {}; + +export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { + const { addError } = useAppToasts(); + const kibana = useKibana(); + const dispatch = useDispatch(); + const [{ from, to }, setRange] = useState({ + from: DEFAULT_EVENT_ENRICHMENT_FROM, + to: DEFAULT_EVENT_ENRICHMENT_TO, + }); + const { error, loading, result, start } = useEventEnrichmentComplete(); + + 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: i18n.INVESTIGATION_ENRICHMENT_REQUEST_ERROR }); + } + }, [addError, error]); + + useEffect(() => { + if (!isEmpty(eventFields)) { + start({ + data: kibana.services.data, + timerange: { from, to, interval: '' }, + defaultIndex: DEFAULT_CTI_SOURCE_INDEX, + eventFields, + filterQuery: '', + }); + } + }, [from, start, kibana.services.data, to, eventFields]); + + return { + loading, + result, + setRange, + }; +}; 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', 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..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 @@ -8,14 +8,16 @@ import { get, isEmpty } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; -import { 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 { + ENRICHMENT_TYPES, + EVENT_ENRICHMENT_INDICATOR_FIELD_MAP, +} from '../../../../../../common/cti/constants'; +import { + CtiEnrichment, + EventField, + isValidEventField, + validEventFields, +} from '../../../../../../common/search_strategy/security_solution/cti'; export const buildIndicatorShouldClauses = ( eventFields: Record @@ -67,6 +69,7 @@ const buildIndicatorMatchedFields = ( 'matched.field': [eventField], 'matched.id': [hit._id], 'matched.index': [hit._index], + 'matched.type': [ENRICHMENT_TYPES.InvestigationTime], }; }; 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', 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_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" }, 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..0598fd7ba7c86 --- /dev/null +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json @@ -0,0 +1,63 @@ +{ + "type": "doc", + "value": { + "id": "a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3", + "index": "filebeat-7.12.0-2021.03.11-000001", + "source": { + "@timestamp": "2021-06-27T14: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", + "ip": "192.168.1.1", + "provider": "another_provider", + "type": "ip" + }, + "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..072318f7f4fc4 --- /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": false + } + }, + "index": "filebeat-7.12.0-2021.03.11-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" + } + } + } +}