From d720235f959b17335c338252fe0cc76dd1332d23 Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Tue, 15 Feb 2022 10:14:36 +0100 Subject: [PATCH] [SecuritySolution] Enrich threshold data from correct fields (#125376) * fix: enrich threshold data from fields data * test: add tests for field edge-cases * test: test cases where value fields are missing Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../event_details/alert_summary_view.test.tsx | 145 +++++++++++++++++- .../event_details/get_alert_summary_rows.tsx | 120 ++++++++++----- 2 files changed, 223 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index 76ca459fbbe1d..25de792731d44 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -254,9 +254,27 @@ describe('AlertSummaryView', () => { }, { category: 'kibana', - field: 'kibana.alert.threshold_result.terms', - values: ['{"field":"host.name","value":"Host-i120rdnmnw"}'], - originalValue: ['{"field":"host.name","value":"Host-i120rdnmnw"}'], + field: 'kibana.alert.threshold_result.terms.value', + values: ['host-23084y2', '3084hf3n84p8934r8h'], + originalValue: ['host-23084y2', '3084hf3n84p8934r8h'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.terms.field', + values: ['host.name', 'host.id'], + originalValue: ['host.name', 'host.id'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.cardinality.field', + values: ['host.name'], + originalValue: ['host.name'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.cardinality.value', + values: [9001], + originalValue: [9001], }, ] as TimelineEventsDetailsItem[]; const renderProps = { @@ -269,11 +287,130 @@ describe('AlertSummaryView', () => { ); - ['Threshold Count', 'host.name [threshold]'].forEach((fieldId) => { + [ + 'Threshold Count', + 'host.name [threshold]', + 'host.id [threshold]', + 'Threshold Cardinality', + 'count(host.name) >= 9001', + ].forEach((fieldId) => { expect(getByText(fieldId)); }); }); + test('Threshold fields are not shown when data is malformated', () => { + const enhancedData = [ + ...mockAlertDetailsData.map((item) => { + if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { + return { + ...item, + values: ['threshold'], + originalValue: ['threshold'], + }; + } + return item; + }), + { + category: 'kibana', + field: 'kibana.alert.threshold_result.count', + values: [9001], + originalValue: [9001], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.terms.field', + // This would be expected to have two entries + values: ['host.id'], + originalValue: ['host.id'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.terms.value', + values: ['host-23084y2', '3084hf3n84p8934r8h'], + originalValue: ['host-23084y2', '3084hf3n84p8934r8h'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.cardinality.field', + values: ['host.name'], + originalValue: ['host.name'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.cardinality.value', + // This would be expected to have one entry + values: [], + originalValue: [], + }, + ] as TimelineEventsDetailsItem[]; + const renderProps = { + ...props, + data: enhancedData, + }; + const { getByText } = render( + + + + ); + + ['Threshold Count'].forEach((fieldId) => { + expect(getByText(fieldId)); + }); + + [ + 'host.name [threshold]', + 'host.id [threshold]', + 'Threshold Cardinality', + 'count(host.name) >= 9001', + ].forEach((fieldText) => { + expect(() => getByText(fieldText)).toThrow(); + }); + }); + + test('Threshold fields are not shown when data is partially missing', () => { + const enhancedData = [ + ...mockAlertDetailsData.map((item) => { + if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { + return { + ...item, + values: ['threshold'], + originalValue: ['threshold'], + }; + } + return item; + }), + { + category: 'kibana', + field: 'kibana.alert.threshold_result.terms.field', + // This would be expected to have two entries + values: ['host.id'], + originalValue: ['host.id'], + }, + { + category: 'kibana', + field: 'kibana.alert.threshold_result.cardinality.field', + values: ['host.name'], + originalValue: ['host.name'], + }, + ] as TimelineEventsDetailsItem[]; + const renderProps = { + ...props, + data: enhancedData, + }; + const { getByText } = render( + + + + ); + + // The `value` fields are missing here, so the enriched field info cannot be calculated correctly + ['host.id [threshold]', 'Threshold Cardinality', 'count(host.name) >= 9001'].forEach( + (fieldText) => { + expect(() => getByText(fieldText)).toThrow(); + } + ); + }); + test("doesn't render empty fields", () => { const renderProps = { ...props, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index 441bd5028cb95..3da4ecab77992 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { getOr, find, isEmpty, uniqBy } from 'lodash/fp'; +import { find, isEmpty, uniqBy } from 'lodash/fp'; import { ALERT_RULE_NAMESPACE, ALERT_RULE_TYPE, @@ -24,12 +24,18 @@ import { import { ALERT_THRESHOLD_RESULT } from '../../../../common/field_maps/field_names'; import { AGENT_STATUS_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; import { getEnrichedFieldInfo, SummaryRow } from './helpers'; -import { EventSummaryField } from './types'; +import { EventSummaryField, EnrichedFieldInfo } from './types'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check'; import { EventCode, EventCategory } from '../../../../common/ecs/event'; +const THRESHOLD_TERMS_FIELD = `${ALERT_THRESHOLD_RESULT}.terms.field`; +const THRESHOLD_TERMS_VALUE = `${ALERT_THRESHOLD_RESULT}.terms.value`; +const THRESHOLD_CARDINALITY_FIELD = `${ALERT_THRESHOLD_RESULT}.cardinality.field`; +const THRESHOLD_CARDINALITY_VALUE = `${ALERT_THRESHOLD_RESULT}.cardinality.value`; +const THRESHOLD_COUNT = `${ALERT_THRESHOLD_RESULT}.count`; + /** Always show these fields */ const alwaysDisplayedFields: EventSummaryField[] = [ { id: 'host.name' }, @@ -132,10 +138,10 @@ function getFieldsByRuleType(ruleType?: string): EventSummaryField[] { switch (ruleType) { case 'threshold': return [ - { id: `${ALERT_THRESHOLD_RESULT}.count`, label: ALERTS_HEADERS_THRESHOLD_COUNT }, - { id: `${ALERT_THRESHOLD_RESULT}.terms`, label: ALERTS_HEADERS_THRESHOLD_TERMS }, + { id: THRESHOLD_COUNT, label: ALERTS_HEADERS_THRESHOLD_COUNT }, + { id: THRESHOLD_TERMS_FIELD, label: ALERTS_HEADERS_THRESHOLD_TERMS }, { - id: `${ALERT_THRESHOLD_RESULT}.cardinality`, + id: THRESHOLD_CARDINALITY_FIELD, label: ALERTS_HEADERS_THRESHOLD_CARDINALITY, }, ]; @@ -272,42 +278,20 @@ export const getSummaryRows = ({ return acc; } - if (field.id === `${ALERT_THRESHOLD_RESULT}.terms`) { - try { - const terms = getOr(null, 'originalValue', item); - const parsedValue = terms.map((term: string) => JSON.parse(term)); - const thresholdTerms = (parsedValue ?? []).map( - (entry: { field: string; value: string }) => { - return { - title: `${entry.field} [threshold]`, - description: { - ...description, - values: [entry.value], - }, - }; - } - ); - return [...acc, ...thresholdTerms]; - } catch (err) { - return [...acc]; + if (field.id === THRESHOLD_TERMS_FIELD) { + const enrichedInfo = enrichThresholdTerms(item, data, description); + if (enrichedInfo) { + return [...acc, ...enrichedInfo]; + } else { + return acc; } } - if (field.id === `${ALERT_THRESHOLD_RESULT}.cardinality`) { - try { - const value = getOr(null, 'originalValue.0', field); - const parsedValue = JSON.parse(value); - return [ - ...acc, - { - title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, - description: { - ...description, - values: [`count(${parsedValue.field}) == ${parsedValue.value}`], - }, - }, - ]; - } catch (err) { + if (field.id === THRESHOLD_CARDINALITY_FIELD) { + const enrichedInfo = enrichThresholdCardinality(item, data, description); + if (enrichedInfo) { + return [...acc, enrichedInfo]; + } else { return acc; } } @@ -322,3 +306,63 @@ export const getSummaryRows = ({ }, []) : []; }; + +/** + * Enriches the summary data for threshold terms. + * For any given threshold term, it generates a row with the term's name and the associated value. + */ +function enrichThresholdTerms( + { values: termsFieldArr }: TimelineEventsDetailsItem, + data: TimelineEventsDetailsItem[], + description: EnrichedFieldInfo +) { + const termsValueItem = data.find((d) => d.field === THRESHOLD_TERMS_VALUE); + const termsValueArray = termsValueItem && termsValueItem.values; + + // Make sure both `fields` and `values` are an array and that they have the same length + if ( + Array.isArray(termsFieldArr) && + termsFieldArr.length > 0 && + Array.isArray(termsValueArray) && + termsFieldArr.length === termsValueArray.length + ) { + return termsFieldArr.map((field, index) => { + return { + title: `${field} [threshold]`, + description: { + ...description, + values: [termsValueArray[index]], + }, + }; + }); + } +} + +/** + * Enriches the summary data for threshold cardinality. + * Reads out the cardinality field and the value and interpolates them into a combined string value. + */ +function enrichThresholdCardinality( + { values: cardinalityFieldArr }: TimelineEventsDetailsItem, + data: TimelineEventsDetailsItem[], + description: EnrichedFieldInfo +) { + const cardinalityValueItem = data.find((d) => d.field === THRESHOLD_CARDINALITY_VALUE); + const cardinalityValueArray = cardinalityValueItem && cardinalityValueItem.values; + + // Only return a summary row if we actually have the correct field and value + if ( + Array.isArray(cardinalityFieldArr) && + cardinalityFieldArr.length === 1 && + Array.isArray(cardinalityValueArray) && + cardinalityFieldArr.length === cardinalityValueArray.length + ) { + return { + title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, + description: { + ...description, + values: [`count(${cardinalityFieldArr[0]}) >= ${cardinalityValueArray[0]}`], + }, + }; + } +}