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:
Madison Caldwell 2021-10-29 13:29:38 -04:00 committed by GitHub
parent 30872e9063
commit 6ba984eb03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 266 additions and 2 deletions

View file

@ -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',
}),
}),
}),
],
})
);
});
});

View file

@ -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),
});

View file

@ -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',
});
});
});

View file

@ -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)),
{}
);
};

View file

@ -23,3 +23,6 @@ export const createResultObject = <TState extends AlertTypeState>(state: TState)
};
return result;
};
export * from './expand_dotted';
export * from './get_list_client';

View file

@ -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,
};