Add support for actions on kibana.* fields and legacy signal.* fields (#116491)
* Add support for actions on kibana.* fields and legacy signal.* fields * Improve types and add scheduleNotificationActions test * Unnecessary cast * Was accidentally returning all alerts in map, instead of single alert * Cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
30872e9063
commit
6ba984eb03
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { AlertServicesMock, alertsMock } from '../../../../../alerting/server/mocks';
|
||||
import { sampleThresholdAlert } from '../rule_types/__mocks__/threshold';
|
||||
import {
|
||||
NotificationRuleTypeParams,
|
||||
scheduleNotificationActions,
|
||||
} from './schedule_notification_actions';
|
||||
|
||||
describe('schedule_notification_actions', () => {
|
||||
const alertServices: AlertServicesMock = alertsMock.createAlertServices();
|
||||
const alertId = 'fb30ddd1-5edc-43e2-9afb-3bcd970b78ee';
|
||||
|
||||
const notificationRuleParams: NotificationRuleTypeParams = {
|
||||
author: ['123'],
|
||||
id: '123',
|
||||
name: 'some name',
|
||||
description: '123',
|
||||
buildingBlockType: undefined,
|
||||
from: '123',
|
||||
ruleId: '123',
|
||||
immutable: false,
|
||||
license: '',
|
||||
falsePositives: ['false positive 1', 'false positive 2'],
|
||||
query: 'user.name: root or user.name: admin',
|
||||
language: 'kuery',
|
||||
savedId: 'savedId-123',
|
||||
timelineId: 'timelineid-123',
|
||||
timelineTitle: 'timeline-title-123',
|
||||
meta: {},
|
||||
filters: [],
|
||||
index: ['index-123'],
|
||||
maxSignals: 100,
|
||||
riskScore: 80,
|
||||
riskScoreMapping: [],
|
||||
ruleNameOverride: undefined,
|
||||
outputIndex: 'output-1',
|
||||
severity: 'high',
|
||||
severityMapping: [],
|
||||
threat: [],
|
||||
timestampOverride: undefined,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
references: ['http://www.example.com'],
|
||||
namespace: 'a namespace',
|
||||
note: '# sample markdown',
|
||||
version: 1,
|
||||
exceptionsList: [],
|
||||
};
|
||||
|
||||
it('Should schedule actions with unflatted and legacy context', () => {
|
||||
const alertInstance = alertServices.alertInstanceFactory(alertId);
|
||||
const signals = [sampleThresholdAlert._source, sampleThresholdAlert._source];
|
||||
scheduleNotificationActions({
|
||||
alertInstance,
|
||||
signalsCount: 2,
|
||||
resultsLink: '',
|
||||
ruleParams: notificationRuleParams,
|
||||
signals,
|
||||
});
|
||||
expect(alertInstance.scheduleActions).toHaveBeenCalledWith(
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
alerts: [
|
||||
expect.objectContaining({
|
||||
kibana: expect.objectContaining({
|
||||
alert: expect.objectContaining({
|
||||
rule: expect.objectContaining({
|
||||
uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
signal: expect.objectContaining({
|
||||
rule: expect.objectContaining({
|
||||
id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kibana: expect.objectContaining({
|
||||
alert: expect.objectContaining({
|
||||
rule: expect.objectContaining({
|
||||
uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
signal: expect.objectContaining({
|
||||
rule: expect.objectContaining({
|
||||
id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -7,13 +7,44 @@
|
|||
|
||||
import { mapKeys, snakeCase } from 'lodash/fp';
|
||||
import { AlertInstance } from '../../../../../alerting/server';
|
||||
import { expandDottedObject } from '../rule_types/utils';
|
||||
import { RuleParams } from '../schemas/rule_schemas';
|
||||
import aadFieldConversion from '../routes/index/signal_aad_mapping.json';
|
||||
import { isRACAlert } from '../signals/utils';
|
||||
import { RACAlert } from '../rule_types/types';
|
||||
|
||||
export type NotificationRuleTypeParams = RuleParams & {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const convertToLegacyAlert = (alert: RACAlert) =>
|
||||
Object.entries(aadFieldConversion).reduce((acc, [legacyField, aadField]) => {
|
||||
const val = alert[aadField];
|
||||
if (val != null) {
|
||||
return {
|
||||
...acc,
|
||||
[legacyField]: val,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/*
|
||||
* Formats alerts before sending to `scheduleActions`. We augment the context with
|
||||
* the equivalent "legacy" alert context so that pre-8.0 actions will continue to work.
|
||||
*/
|
||||
const formatAlertsForNotificationActions = (alerts: unknown[]) => {
|
||||
return alerts.map((alert) =>
|
||||
isRACAlert(alert)
|
||||
? {
|
||||
...expandDottedObject(convertToLegacyAlert(alert)),
|
||||
...expandDottedObject(alert),
|
||||
}
|
||||
: alert
|
||||
);
|
||||
};
|
||||
|
||||
interface ScheduleNotificationActions {
|
||||
alertInstance: AlertInstance;
|
||||
signalsCount: number;
|
||||
|
@ -36,5 +67,5 @@ export const scheduleNotificationActions = ({
|
|||
.scheduleActions('default', {
|
||||
results_link: resultsLink,
|
||||
rule: mapKeys(snakeCase, ruleParams),
|
||||
alerts: signals,
|
||||
alerts: formatAlertsForNotificationActions(signals),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { expandDottedObject } from './expand_dotted';
|
||||
|
||||
describe('Expand Dotted', () => {
|
||||
it('expands simple dotted fields to nested objects', () => {
|
||||
const simpleDottedObj = {
|
||||
'kibana.test.1': 'the spice must flow',
|
||||
'kibana.test.2': 2,
|
||||
'kibana.test.3': null,
|
||||
};
|
||||
expect(expandDottedObject(simpleDottedObj)).toEqual({
|
||||
kibana: {
|
||||
test: {
|
||||
1: 'the spice must flow',
|
||||
2: 2,
|
||||
3: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('expands complex dotted fields to nested objects', () => {
|
||||
const complexDottedObj = {
|
||||
'kibana.test.1': 'the spice must flow',
|
||||
'kibana.test.2': ['a', 'b', 'c', 'd'],
|
||||
'kibana.test.3': null,
|
||||
'signal.test': {
|
||||
key: 'val',
|
||||
},
|
||||
'kibana.alert.ancestors': [
|
||||
{
|
||||
name: 'ancestor1',
|
||||
},
|
||||
{
|
||||
name: 'ancestor2',
|
||||
},
|
||||
],
|
||||
flat: 'yep',
|
||||
};
|
||||
expect(expandDottedObject(complexDottedObj)).toEqual({
|
||||
kibana: {
|
||||
alert: {
|
||||
ancestors: [
|
||||
{
|
||||
name: 'ancestor1',
|
||||
},
|
||||
{
|
||||
name: 'ancestor2',
|
||||
},
|
||||
],
|
||||
},
|
||||
test: {
|
||||
1: 'the spice must flow',
|
||||
2: ['a', 'b', 'c', 'd'],
|
||||
3: null,
|
||||
},
|
||||
},
|
||||
signal: {
|
||||
test: {
|
||||
key: 'val',
|
||||
},
|
||||
},
|
||||
flat: 'yep',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { merge } from '@kbn/std';
|
||||
|
||||
const expandDottedField = (dottedFieldName: string, val: unknown): object => {
|
||||
const parts = dottedFieldName.split('.');
|
||||
if (parts.length === 1) {
|
||||
return { [parts[0]]: val };
|
||||
} else {
|
||||
return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) };
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Expands an object with "dotted" fields to a nested object with unflattened fields.
|
||||
*
|
||||
* Example:
|
||||
* expandDottedObject({
|
||||
* "kibana.alert.depth": 1,
|
||||
* "kibana.alert.ancestors": [{
|
||||
* id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71",
|
||||
* type: "event",
|
||||
* index: "signal_index",
|
||||
* depth: 0,
|
||||
* }],
|
||||
* })
|
||||
*
|
||||
* => {
|
||||
* kibana: {
|
||||
* alert: {
|
||||
* ancestors: [
|
||||
* id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71",
|
||||
* type: "event",
|
||||
* index: "signal_index",
|
||||
* depth: 0,
|
||||
* ],
|
||||
* depth: 1,
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
export const expandDottedObject = (dottedObj: object) => {
|
||||
return Object.entries(dottedObj).reduce(
|
||||
(acc, [key, val]) => merge(acc, expandDottedField(key, val)),
|
||||
{}
|
||||
);
|
||||
};
|
|
@ -23,3 +23,6 @@ export const createResultObject = <TState extends AlertTypeState>(state: TState)
|
|||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
export * from './expand_dotted';
|
||||
export * from './get_list_client';
|
||||
|
|
|
@ -58,7 +58,7 @@ import {
|
|||
ThreatRuleParams,
|
||||
ThresholdRuleParams,
|
||||
} from '../schemas/rule_schemas';
|
||||
import { WrappedRACAlert } from '../rule_types/types';
|
||||
import { RACAlert, WrappedRACAlert } from '../rule_types/types';
|
||||
import { SearchTypes } from '../../../../common/detection_engine/types';
|
||||
import { IRuleExecutionLogClient } from '../rule_execution_log/types';
|
||||
interface SortExceptionsReturn {
|
||||
|
@ -985,6 +985,10 @@ export const isWrappedRACAlert = (event: SimpleHit): event is WrappedRACAlert =>
|
|||
return (event as WrappedRACAlert)?._source?.[ALERT_UUID] != null;
|
||||
};
|
||||
|
||||
export const isRACAlert = (event: unknown): event is RACAlert => {
|
||||
return (event as RACAlert)?.[ALERT_UUID] != null;
|
||||
};
|
||||
|
||||
export const racFieldMappings: Record<string, string> = {
|
||||
'signal.rule.id': ALERT_RULE_UUID,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue