mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][CTI] Enable rendering of CTI indicators with flattened fields (#179395)
## Summary Our initial implementation of these components assumed a very flat, normal structure for the indicator documents we would retrieve (because we leverage the `fields` API). However, `flattened` fields do not quite fit this pattern, and there is a bug where indicator documents containing `flattened` fields with complex values would not be parsed correctly, and we attempt to render JS objects to the DOM (which React does not like, and throws an error). This issue was uncovered originally in an SDH. ### How to Review See https://github.com/elastic/kibana/issues/179483 for details on how to repro. ### Screenshots (Using the data described in https://github.com/elastic/kibana/issues/179483): <img width="820" alt="Screenshot 2024-03-26 at 3 28 00 PM" src="af62724d
-6626-4b61-91b8-48612889a109"> <img width="820" alt="Screenshot 2024-03-26 at 3 28 15 PM" src="9208e7bd
-c149-44a3-9a56-4a2813d79ad7"> Linked issue: https://github.com/elastic/kibana/issues/179483
This commit is contained in:
parent
a7cea13300
commit
184b6e2ad4
6 changed files with 351 additions and 18 deletions
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This represents an indicator document with an array of objects as field
|
||||
* values. This shape of indicator was previously causing render errors in the
|
||||
* CTI UI.
|
||||
*/
|
||||
export const indicatorWithNestedObjects = {
|
||||
'threat.indicator.type': ['ipv4-addr'],
|
||||
'elastic_agent.version': ['8.10.4'],
|
||||
'event.category': ['threat'],
|
||||
'recordedfuture.risk_string': ['7/75'],
|
||||
'threat.indicator.provider': [
|
||||
'Mastodon',
|
||||
'Twitter',
|
||||
'Recorded Future Command & Control Reports',
|
||||
'Recorded Future Sandbox - Malware C2 Extractions',
|
||||
'GitHub',
|
||||
'Recorded Future Command & Control Validation',
|
||||
'Malware Patrol',
|
||||
'Polyswarm Sandbox Analysis - Malware C2 Extractions',
|
||||
'Recorded Future Triage Malware Analysis - Malware C2 Extractions',
|
||||
],
|
||||
'agent.type': ['filebeat'],
|
||||
'agent.name': ['win-10'],
|
||||
'elastic_agent.snapshot': [false],
|
||||
'event.agent_id_status': ['verified'],
|
||||
'event.kind': ['enrichment'],
|
||||
'threat.feed.name': ['Recorded Future'],
|
||||
'elastic_agent.id': ['e8ffaf42-7436-4e39-b895-772bb86e6585'],
|
||||
'recordedfuture.name': ['188.116.21.141'],
|
||||
'data_stream.namespace': ['default'],
|
||||
'recordedfuture.evidence_details': [
|
||||
{
|
||||
SourcesCount: 2,
|
||||
SightingsCount: 2,
|
||||
CriticalityLabel: 'Unusual',
|
||||
Rule: 'Recently Reported as a Defanged IP',
|
||||
EvidenceString:
|
||||
'2 sightings on 2 sources: Mastodon, Twitter. Most recent link (Feb 13, 2024): https://ioc.exchange/@SarlackLab/111926194382069197',
|
||||
Sources: ['source:pupSAn', 'source:BV5'],
|
||||
Timestamp: '2024-02-13T21:03:10.000Z',
|
||||
Name: 'recentDefanged',
|
||||
MitigationString: '',
|
||||
Criticality: 1,
|
||||
},
|
||||
{
|
||||
SourcesCount: 2,
|
||||
SightingsCount: 12,
|
||||
CriticalityLabel: 'Suspicious',
|
||||
Rule: 'Historically Reported C&C Server',
|
||||
EvidenceString:
|
||||
'12 sightings on 2 sources: Recorded Future Command & Control Reports, Recorded Future Sandbox - Malware C2 Extractions. 188.116.21.141:20213 was reported as a command and control server for RedLine Stealer on Feb 10, 2024',
|
||||
Sources: ['source:qU_q-9', 'source:oWAG20'],
|
||||
Timestamp: '2024-02-10T08:22:27.790Z',
|
||||
Name: 'reportedCnc',
|
||||
MitigationString: '',
|
||||
Criticality: 2,
|
||||
},
|
||||
{
|
||||
SourcesCount: 1,
|
||||
SightingsCount: 2,
|
||||
CriticalityLabel: 'Suspicious',
|
||||
Rule: 'Recently Linked to Intrusion Method',
|
||||
EvidenceString:
|
||||
'2 sightings on 1 source: GitHub. 6 related intrusion methods including DDOS Toolkit, njRAT, Phishing, Remote Access Trojan, Stealware. Most recent link (Feb 13, 2024): https://github.com/0xDanielLopez/TweetFeed/commit/fd64eaa71f7e948d1cca1dc8c148b6515e878df5',
|
||||
Sources: ['source:MIKjae'],
|
||||
Timestamp: '2024-02-13T21:57:24.894Z',
|
||||
Name: 'recentLinkedIntrusion',
|
||||
MitigationString: '',
|
||||
Criticality: 2,
|
||||
},
|
||||
{
|
||||
SourcesCount: 1,
|
||||
SightingsCount: 11,
|
||||
CriticalityLabel: 'Suspicious',
|
||||
Rule: 'Previously Validated C&C Server',
|
||||
EvidenceString:
|
||||
'11 sightings on 1 source: Recorded Future Command & Control Validation. Recorded Future analysis validated 188.116.21.141:20213 as a command and control server for RedLine Stealer on Feb 22, 2024',
|
||||
Sources: ['source:qGriFQ'],
|
||||
Timestamp: '2024-02-22T00:06:26.000Z',
|
||||
Name: 'validatedCnc',
|
||||
MitigationString: '',
|
||||
Criticality: 2,
|
||||
},
|
||||
{
|
||||
SourcesCount: 1,
|
||||
SightingsCount: 1,
|
||||
CriticalityLabel: 'Suspicious',
|
||||
Rule: 'Recent Suspected C&C Server',
|
||||
EvidenceString:
|
||||
'1 sighting on 1 source: Malware Patrol. Malware Patrol identified 188.116.21.141:20213 as a command and control server for RecordBreaker Stealer on February 14, 2024.',
|
||||
Sources: ['source:qs_-cU'],
|
||||
Timestamp: '2024-02-14T10:55:01.908Z',
|
||||
Name: 'recentSuspectedCnc',
|
||||
MitigationString: '',
|
||||
Criticality: 2,
|
||||
},
|
||||
{
|
||||
SourcesCount: 4,
|
||||
SightingsCount: 26,
|
||||
CriticalityLabel: 'Malicious',
|
||||
Rule: 'Recently Reported C&C Server',
|
||||
EvidenceString:
|
||||
'26 sightings on 4 sources: Polyswarm Sandbox Analysis - Malware C2 Extractions, Recorded Future Command & Control Reports, Recorded Future Triage Malware Analysis - Malware C2 Extractions, Recorded Future Sandbox - Malware C2 Extractions. 188.116.21.141:20213 was reported as a command and control server for Redline Stealer on Feb 21, 2024',
|
||||
Sources: ['source:hyihHO', 'source:qU_q-9', 'source:nTcIsu', 'source:oWAG20'],
|
||||
Timestamp: '2024-02-21T08:22:44.811Z',
|
||||
Name: 'recentReportedCnc',
|
||||
MitigationString: '',
|
||||
Criticality: 3,
|
||||
},
|
||||
{
|
||||
SourcesCount: 1,
|
||||
SightingsCount: 3,
|
||||
CriticalityLabel: 'Very Malicious',
|
||||
Rule: 'Validated C&C Server',
|
||||
EvidenceString:
|
||||
'3 sightings on 1 source: Recorded Future Command & Control Validation. Recorded Future analysis validated 188.116.21.141:20213 as a command and control server for RedLine Stealer on Feb 24, 2024',
|
||||
Sources: ['source:qGriFQ'],
|
||||
Timestamp: '2024-02-24T00:52:16.000Z',
|
||||
Name: 'recentValidatedCnc',
|
||||
MitigationString: '',
|
||||
Criticality: 4,
|
||||
},
|
||||
],
|
||||
'input.type': ['httpjson'],
|
||||
'data_stream.type': ['logs'],
|
||||
'event.risk_score': [98],
|
||||
tags: ['forwarded', 'recordedfuture'],
|
||||
'event.ingested': ['2024-02-24T17:32:40.000Z'],
|
||||
'@timestamp': ['2024-02-24T17:32:37.813Z'],
|
||||
'agent.id': ['e8ffaf42-7436-4e39-b895-772bb86e6585'],
|
||||
'threat.indicator.ip': ['188.116.21.141'],
|
||||
'ecs.version': ['8.11.0'],
|
||||
'data_stream.dataset': ['ti_recordedfuture.threat'],
|
||||
'event.created': ['2024-02-24T17:32:37.813Z'],
|
||||
'event.type': ['indicator'],
|
||||
'agent.ephemeral_id': ['0532c813-1434-4c76-800b-6abdf7eaf62c'],
|
||||
'agent.version': ['8.10.4'],
|
||||
'event.dataset': ['ti_recordedfuture.threat'],
|
||||
} as const;
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { EnrichmentAccordionGroup } from './enrichment_accordion_group';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { indicatorWithNestedObjects } from '../__mocks__/indicator_with_nested_objects';
|
||||
import type { CtiEnrichment } from '../../../../../common/search_strategy';
|
||||
|
||||
describe('EnrichmentAccordionGroup', () => {
|
||||
describe('with an indicator with an array of nested objects as a field value', () => {
|
||||
it('renders the indicator without those fields', () => {
|
||||
// @ts-expect-error this indicator intentionally does not conform to the CtiEnrichment type
|
||||
const enrichments = [indicatorWithNestedObjects] as CtiEnrichment[];
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<EnrichmentAccordionGroup enrichments={enrichments} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const enrichmentView = getByTestId('threat-details-view-0');
|
||||
|
||||
expect(enrichmentView).toBeInTheDocument();
|
||||
expect(enrichmentView).toHaveTextContent('ipv4-addr');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,7 +18,12 @@ import {
|
|||
|
||||
import type { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti';
|
||||
import type { ThreatDetailsRow } from './helpers';
|
||||
import { getEnrichmentIdentifiers, isInvestigationTimeEnrichment, getFirstSeen } from './helpers';
|
||||
import {
|
||||
getEnrichmentIdentifiers,
|
||||
isInvestigationTimeEnrichment,
|
||||
getFirstSeen,
|
||||
buildThreatDetailsItems,
|
||||
} from './helpers';
|
||||
import { EnrichmentButtonContent } from './enrichment_button_content';
|
||||
import { ThreatSummaryTitle } from './threat_summary_title';
|
||||
import { InspectButton } from '../../inspect';
|
||||
|
@ -26,8 +31,6 @@ import { QUERY_ID } from '../../../containers/cti/event_enrichment';
|
|||
import * as i18n from './translations';
|
||||
import { ThreatSummaryTable } from './threat_summary_table';
|
||||
import { REFERENCE } from '../../../../../common/cti/constants';
|
||||
import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants';
|
||||
import { getFirstElement } from '../../../../../common/utils/data_retrieval';
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
.euiAccordion__triggerWrapper {
|
||||
|
@ -82,19 +85,6 @@ const columns: Array<EuiBasicTableColumn<ThreatDetailsRow>> = [
|
|||
},
|
||||
];
|
||||
|
||||
const buildThreatDetailsItems = (enrichment: CtiEnrichment) =>
|
||||
Object.keys(enrichment)
|
||||
.sort()
|
||||
.map((field) => ({
|
||||
title: field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH)
|
||||
? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}`, 'indicator')
|
||||
: field,
|
||||
description: {
|
||||
fieldName: field,
|
||||
value: getFirstElement(enrichment[field]),
|
||||
},
|
||||
}));
|
||||
|
||||
const EnrichmentAccordion: React.FC<{
|
||||
enrichment: CtiEnrichment;
|
||||
index: number;
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
getEnrichmentFields,
|
||||
parseExistingEnrichments,
|
||||
getEnrichmentIdentifiers,
|
||||
buildThreatDetailsItems,
|
||||
} from './helpers';
|
||||
|
||||
describe('parseExistingEnrichments', () => {
|
||||
|
@ -492,3 +493,133 @@ describe('getEnrichmentIdentifiers', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildThreatDetailsItems', () => {
|
||||
it('returns an empty array if given an empty enrichment', () => {
|
||||
expect(buildThreatDetailsItems({})).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns an array of threat details items', () => {
|
||||
const enrichment = {
|
||||
'matched.field': ['matched field'],
|
||||
'matched.atomic': ['matched atomic'],
|
||||
'matched.type': ['matched type'],
|
||||
'feed.name': ['feed name'],
|
||||
};
|
||||
expect(buildThreatDetailsItems(enrichment)).toEqual([
|
||||
{
|
||||
description: {
|
||||
fieldName: 'feed.name',
|
||||
value: 'feed name',
|
||||
},
|
||||
title: 'feed.name',
|
||||
},
|
||||
{
|
||||
description: {
|
||||
fieldName: 'matched.atomic',
|
||||
value: 'matched atomic',
|
||||
},
|
||||
title: 'matched.atomic',
|
||||
},
|
||||
{
|
||||
description: {
|
||||
fieldName: 'matched.field',
|
||||
value: 'matched field',
|
||||
},
|
||||
title: 'matched.field',
|
||||
},
|
||||
{
|
||||
description: {
|
||||
fieldName: 'matched.type',
|
||||
value: 'matched type',
|
||||
},
|
||||
title: 'matched.type',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('retrieves the first value of an array field', () => {
|
||||
const enrichment = {
|
||||
array_values: ['first value', 'second value'],
|
||||
};
|
||||
|
||||
expect(buildThreatDetailsItems(enrichment)).toEqual([
|
||||
{
|
||||
title: 'array_values',
|
||||
description: {
|
||||
fieldName: 'array_values',
|
||||
value: 'first value',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('shortens indicator field names if they contain the default indicator path', () => {
|
||||
const enrichment = {
|
||||
'threat.indicator.ip': ['127.0.0.1'],
|
||||
};
|
||||
expect(buildThreatDetailsItems(enrichment)).toEqual([
|
||||
{
|
||||
title: 'indicator.ip',
|
||||
description: {
|
||||
fieldName: 'threat.indicator.ip',
|
||||
value: '127.0.0.1',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses an object field', () => {
|
||||
const enrichment = {
|
||||
'object_field.foo': ['bar'],
|
||||
};
|
||||
|
||||
expect(buildThreatDetailsItems(enrichment)).toEqual([
|
||||
{
|
||||
title: 'object_field.foo',
|
||||
description: {
|
||||
fieldName: 'object_field.foo',
|
||||
value: 'bar',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
describe('field responses for fields of type "flattened"', () => {
|
||||
it('returns a note for the value of a flattened field containing a single object', () => {
|
||||
const enrichment = {
|
||||
flattened_object: [{ foo: 'bar' }],
|
||||
};
|
||||
|
||||
expect(buildThreatDetailsItems(enrichment)).toEqual([
|
||||
{
|
||||
title: 'flattened_object',
|
||||
description: {
|
||||
fieldName: 'flattened_object',
|
||||
value:
|
||||
'This field contains nested object values, which are not rendered here. See the full document for all fields/values',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a note for the value of a flattened field containing an array of objects', () => {
|
||||
const enrichment = {
|
||||
array_field: [{ foo: 'bar' }, { baz: 'qux' }],
|
||||
};
|
||||
|
||||
expect(buildThreatDetailsItems(enrichment)).toEqual([
|
||||
{
|
||||
title: 'array_field',
|
||||
description: {
|
||||
fieldName: 'array_field',
|
||||
value:
|
||||
'This field contains nested object values, which are not rendered here. See the full document for all fields/values',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { groupBy } from 'lodash';
|
||||
import { groupBy, isObject } from 'lodash';
|
||||
import { getDataFromFieldsHits } from '../../../../../common/utils/field_formatters';
|
||||
import { ENRICHMENT_DESTINATION_PATH } from '../../../../../common/constants';
|
||||
import {
|
||||
DEFAULT_INDICATOR_SOURCE_PATH,
|
||||
ENRICHMENT_DESTINATION_PATH,
|
||||
} from '../../../../../common/constants';
|
||||
import {
|
||||
ENRICHMENT_TYPES,
|
||||
FIRST_SEEN,
|
||||
|
@ -25,6 +28,7 @@ import type {
|
|||
} from '../../../../../common/search_strategy/security_solution/cti';
|
||||
import { isValidEventField } from '../../../../../common/search_strategy/security_solution/cti';
|
||||
import { getFirstElement } from '../../../../../common/utils/data_retrieval';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const isInvestigationTimeEnrichment = (type: string | undefined) =>
|
||||
type === ENRICHMENT_TYPES.InvestigationTime;
|
||||
|
@ -134,3 +138,24 @@ export interface ThreatDetailsRow {
|
|||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ThreatDetailItem {
|
||||
title: string;
|
||||
description: { fieldName: string; value: unknown };
|
||||
}
|
||||
|
||||
export const buildThreatDetailsItems = (enrichment: CtiEnrichment): ThreatDetailItem[] =>
|
||||
Object.keys(enrichment)
|
||||
.sort()
|
||||
.map((field) => {
|
||||
const title = field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH)
|
||||
? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}`, 'indicator')
|
||||
: field;
|
||||
|
||||
let value = getFirstElement(enrichment[field]);
|
||||
if (isObject(value)) {
|
||||
value = i18n.NESTED_OBJECT_VALUES_NOT_RENDERED;
|
||||
}
|
||||
|
||||
return { title, description: { fieldName: field, value } };
|
||||
});
|
||||
|
|
|
@ -92,6 +92,14 @@ export const ENRICHED_DATA = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const NESTED_OBJECT_VALUES_NOT_RENDERED = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentObjectValuesNotRendered',
|
||||
{
|
||||
defaultMessage:
|
||||
'This field contains nested object values, which are not rendered here. See the full document for all fields/values',
|
||||
}
|
||||
);
|
||||
|
||||
export const CURRENT_RISK_LEVEL = (riskEntity: RiskScoreEntity) =>
|
||||
i18n.translate('xpack.securitySolution.alertDetails.overview.hostRiskLevel', {
|
||||
defaultMessage: 'Current {riskEntity} risk level',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue