mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* 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>
(cherry picked from commit d720235f95
)
Co-authored-by: Jan Monschke <jan.monschke@elastic.co>
This commit is contained in:
parent
fafb44c967
commit
7a2326fb88
2 changed files with 223 additions and 42 deletions
|
@ -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', () => {
|
|||
</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,
|
||||
|
|
|
@ -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]}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue