Skip to content

Commit

Permalink
[SecuritySolution] Enrich threshold data from correct fields (elastic…
Browse files Browse the repository at this point in the history
…#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 <[email protected]>
  • Loading branch information
janmonschke and kibanamachine authored Feb 15, 2022
1 parent 7c2437f commit d720235
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -269,11 +287,130 @@ describe('AlertSummaryView', () => {
</TestProvidersComponent>
);

['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(
<TestProvidersComponent>
<AlertSummaryView {...renderProps} />
</TestProvidersComponent>
);

['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(
<TestProvidersComponent>
<AlertSummaryView {...renderProps} />
</TestProvidersComponent>
);

// 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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' },
Expand Down Expand Up @@ -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,
},
];
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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]}`],
},
};
}
}

0 comments on commit d720235

Please sign in to comment.