Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][CTI] Investigation time enrichment UI #103383

Merged
merged 42 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2b68cc3
Add pure fn and consuming hook to fetch event enrichment
rylnd Jun 18, 2021
a85aa96
Move existing enrichment tests to new spec file
rylnd Jun 21, 2021
7978796
Move test constants into tests that use them
rylnd Jun 21, 2021
18b22fc
style: declare FC function as an FC
rylnd Jun 22, 2021
7d6ffc9
Extract some inline parsing logic into a helper function
rylnd Jun 22, 2021
3dafc7c
Solidifying enrichment types on the backend
rylnd Jun 24, 2021
d7e6f9d
WIP: Enrichment rows are rendered on the alerts summary
rylnd Jun 24, 2021
7216655
Updates ThreatDetailsView to accept an array of enrichments
rylnd Jun 24, 2021
ebd1b9d
Sort our details fields
rylnd Jun 24, 2021
dcfa72c
Add "view threat intel data" button
rylnd Jun 24, 2021
1e1690f
Implement header for threat details sections
rylnd Jun 24, 2021
2a76394
Add a basic jest "unit" test around ThreatSummaryView
rylnd Jun 25, 2021
6a2adf3
Fix remaining tests for components we modified
rylnd Jun 25, 2021
c823df1
Filter out duplicate investigation-time enrichments
rylnd Jun 25, 2021
57e5fe3
Add inspect button to investigation enrichments
rylnd Jun 25, 2021
0bc1eaf
Fix failing unit tests
rylnd Jun 26, 2021
67033bc
Fix existing CTI cypress tests
rylnd Jun 26, 2021
35eb550
Adds a cypress test exercising investigation time enrichment
rylnd Jun 28, 2021
09b7f7c
Populate event enrichment call with actual alert fields
rylnd Jun 28, 2021
7838a5c
Add a new field to our suspicious event to trigger enrichment
rylnd Jun 28, 2021
ba91b57
Only fetch enrichments data if there are valid event fields
rylnd Jun 28, 2021
14a0978
Update enrichments matched.typed in integration tests
rylnd Jun 28, 2021
7193073
Merge branch 'master' into ad_hoc_enrichment_ui
rylnd Jun 28, 2021
f48ffe8
Ensure draggable fields are unique in a multi-match scenario
rylnd Jun 28, 2021
accb629
Simplify types
rylnd Jun 28, 2021
5a94f99
Move helper functioons out of shared location and into consuming comp…
rylnd Jun 28, 2021
18c96fa
Clean up data parsing logic using reduce
rylnd Jun 28, 2021
0196d72
Move our general function into a general location
rylnd Jun 28, 2021
fabefb7
Extract the concept of "enrichment identifiers"
rylnd Jun 28, 2021
81dd927
Use existing constant as the source of our enrichments query
rylnd Jun 28, 2021
48d3f6b
Codify our default enrichment lookback as constants
rylnd Jun 28, 2021
866cb27
Remove unnecessary flexbox
rylnd Jun 29, 2021
f1e843c
Filter out partial responses in the event enrichment observable
rylnd Jun 29, 2021
d2aa4cd
Display placeholders while event enrichment is loading
rylnd Jun 29, 2021
811174c
Update our indicator data to be within the last 30 days
rylnd Jun 29, 2021
3fdd33b
Fix type error with our details tabs
rylnd Jun 29, 2021
9381125
Fix failing jest tests
rylnd Jun 29, 2021
ea1128d
Merge branch 'master' into ad_hoc_enrichment_ui
rylnd Jun 29, 2021
52bdb2f
Merge branch 'master' into ad_hoc_enrichment_ui
kibanamachine Jun 29, 2021
afe8d70
Merge branch 'master' into ad_hoc_enrichment_ui
kibanamachine Jun 29, 2021
8275e05
Skips flaky cypress test
rylnd Jun 30, 2021
e007972
Fix archive mappings to fix cypress test failure
rylnd Jun 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions x-pack/plugins/security_solution/common/cti/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -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}`;
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { IEsSearchResponse } from 'src/plugins/data/public';

import {
CtiEnrichment,
CtiEventEnrichmentRequestOptions,
CtiEventEnrichmentStrategyResponse,
CtiQueries,
Expand Down Expand Up @@ -99,11 +100,63 @@ export const buildEventEnrichmentRawResponseMock = (): IEsSearchResponse => ({
},
});

export const buildEventEnrichmentMock = (
overrides: Partial<CtiEnrichment> = {}
): 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> = {}
): CtiEventEnrichmentStrategyResponse => ({
...buildEventEnrichmentRawResponseMock(),
enrichments: [],
enrichments: [buildEventEnrichmentMock()],
inspect: { dsl: ['{"mocked": "json"}'] },
totalCount: 0,
...overrides,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 '..';

Expand All @@ -18,9 +19,16 @@ export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions {
}

export type CtiEnrichment = Record<string, unknown[]>;
export type EventFields = Record<string, unknown>;

export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse {
enrichments: CtiEnrichment[];
inspect?: Inspect;
inspect: Inspect | null;
rylnd marked this conversation as resolved.
Show resolved Hide resolved
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);
Original file line number Diff line number Diff line change
@@ -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 { DETECTIONS_URL } from '../../urls/navigation';
import { addsFieldsToTimeline } from '../../tasks/rule_details';

describe('CTI Enrichment', () => {
before(() => {
cleanKibana();
esArchiverLoad('threat_indicator');
esArchiverLoad('suspicious_source_event');
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
goToManageAlertsDetectionRules();
createCustomIndicatorRule(newThreatIndicatorRule);
reload();
});

after(() => {
esArchiverUnload('threat_indicator');
esArchiverUnload('suspicious_source_event');
});

beforeEach(() => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
goToManageAlertsDetectionRules();
goToRuleDetails();
});

it('Displays 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<keyof typeof expectedFields>;

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: 'myhash.mysha256',
value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3',
};
const expectedMatches = [indicatorMatchRuleEnrichment, investigationTimeEnrichment];

expandFirstAlert();

cy.get(THREAT_SUMMARY_VIEW).within(() => {
cy.get(TABLE_ROWS).should('have.length', expectedMatches.length);
expectedMatches.forEach((row, index) => {
cy.get(TABLE_ROWS)
.eq(index)
.within(() => {
cy.get(TITLE).should('have.text', row.field);
cy.get(THREAT_CONTENT).should('have.text', row.value);
});
});
});
});
});
});
Loading