fix: enrich threshold data from fields data (#125634)

This commit is contained in:
Jan Monschke 2022-02-15 14:44:59 +01:00 committed by GitHub
parent 2205ea3bc2
commit fe3991fcd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 257 additions and 36 deletions

View file

@ -128,6 +128,183 @@ describe('AlertSummaryView', () => {
expect(container.querySelector('div[data-test-subj="summary-view"]')).toMatchSnapshot();
});
test('Threshold events have special fields', () => {
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.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 = {
...props,
data: enhancedData,
};
const { getByText } = render(
<TestProvidersComponent>
<AlertSummaryView {...renderProps} />
</TestProvidersComponent>
);
[
'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,

View file

@ -28,7 +28,7 @@ import {
SIGNAL_RULE_NAME_FIELD_NAME,
} from '../../../timelines/components/timeline/body/renderers/constants';
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip';
import { SummaryRow } from './helpers';
import { SummaryRow, AlertSummaryRow } from './helpers';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check';
@ -58,8 +58,11 @@ const defaultDisplayFields: EventSummaryField[] = [
{ id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
{ id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
{ id: 'kibana.alert.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT },
{ id: 'kibana.alert.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS },
{ id: 'kibana.alert.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY },
{ id: 'kibana.alert.threshold_result.terms.field', label: ALERTS_HEADERS_THRESHOLD_TERMS },
{
id: 'kibana.alert.threshold_result.cardinality.field',
label: ALERTS_HEADERS_THRESHOLD_CARDINALITY,
},
];
const processCategoryFields: EventSummaryField[] = [
@ -169,7 +172,6 @@ export const getSummaryRows = ({
const linkValueField =
item.linkField != null && data.find((d) => d.field === item.linkField);
const linkValue = getOr(null, 'originalValue.0', linkValueField);
const value = getOr(null, 'originalValue.0', field);
const category = field.category ?? '';
const fieldName = field.field ?? '';
@ -192,41 +194,20 @@ export const getSummaryRows = ({
return acc;
}
if (item.id === 'kibana.alert.threshold_result.terms') {
try {
const terms = getOr(null, 'originalValue', field);
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 (item.id === 'kibana.alert.threshold_result.terms.field') {
const enrichedInfo = enrichThresholdTerms(field, data, description);
if (enrichedInfo) {
return [...acc, ...enrichedInfo];
} else {
return acc;
}
}
if (item.id === 'kibana.alert.threshold_result.cardinality') {
try {
const parsedValue = JSON.parse(value);
return [
...acc,
{
title: ALERTS_HEADERS_THRESHOLD_CARDINALITY,
description: {
...description,
values: [`count(${parsedValue.field}) == ${parsedValue.value}`],
},
},
];
} catch (err) {
if (item.id === 'kibana.alert.threshold_result.cardinality.field') {
const enrichedInfo = enrichThresholdCardinality(field, data, description);
if (enrichedInfo) {
return [...acc, enrichedInfo];
} else {
return acc;
}
}
@ -241,3 +222,66 @@ 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: AlertSummaryRow['description']
): AlertSummaryRow[] | undefined {
const termsValueItem = data.find((d) => d.field === 'kibana.alert.threshold_result.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<AlertSummaryRow>((field, index) => {
const value = termsValueArray[index];
return {
title: `${field} [threshold]`,
description: {
...description,
values: [value],
},
};
});
}
}
/**
* 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: SummaryRow['description']
): SummaryRow | undefined {
const cardinalityValueItem = data.find(
(d) => d.field === 'kibana.alert.threshold_result.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]}`],
},
} as SummaryRow;
}
}