Skip to content

Commit

Permalink
[Security Solution][Detections] Add alert source to detection rule ac…
Browse files Browse the repository at this point in the history
…tion context (#85488)

* Adds context.alerts as available parameter for detection rule actions

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
marshallmain and kibanamachine authored Dec 15, 2020
1 parent 39360a2 commit 818246e
Show file tree
Hide file tree
Showing 21 changed files with 273 additions and 104 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*';
export const DEFAULT_RULE_REFRESH_INTERVAL_ON = true;
export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms
export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms
export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100;

export enum SecurityPageName {
detections = 'detections',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export const getActionMessageParams = memoizeOne(
description: 'context.results_link',
useWithTripleBracesInTemplates: true,
},
{ name: 'alerts', description: 'context.alerts' },
...actionMessageRuleParams.map((param) => {
const extendedParam = `rule.${param}`;
return { name: extendedParam, description: `context.${extendedParam}` };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ interface BuildSignalsSearchQuery {
index: string;
from: string;
to: string;
size?: number;
}

export const buildSignalsSearchQuery = ({ ruleId, index, from, to }: BuildSignalsSearchQuery) => ({
export const buildSignalsSearchQuery = ({
ruleId,
index,
from,
to,
size,
}: BuildSignalsSearchQuery) => ({
index,
body: {
size,
query: {
bool: {
filter: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { AlertServices } from '../../../../../alerts/server';
import { SignalSearchResponse } from '../signals/types';
import { buildSignalsSearchQuery } from './build_signals_query';

interface GetSignalsParams {
from?: string;
to?: string;
size?: number;
ruleId: string;
index: string;
callCluster: AlertServices['callCluster'];
}

export const getSignals = async ({
from,
to,
size,
ruleId,
index,
callCluster,
}: GetSignalsParams): Promise<SignalSearchResponse> => {
if (from == null || to == null) {
throw Error('"from" or "to" was not provided to signals query');
}

const query = buildSignalsSearchQuery({
index,
ruleId,
to,
from,
size,
});

const result: SignalSearchResponse = await callCluster('search', query);

return result;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { rulesNotificationAlertType } from './rules_notification_alert_type';
import { buildSignalsSearchQuery } from './build_signals_query';
import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks';
import { NotificationExecutorOptions } from './types';
import {
sampleDocSearchResultsNoSortIdNoVersion,
sampleDocSearchResultsWithSortId,
sampleEmptyDocSearchResults,
} from '../signals/__mocks__/es_results';
import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants';
jest.mock('./build_signals_query');

describe('rules_notification_alert_type', () => {
Expand Down Expand Up @@ -63,9 +69,7 @@ describe('rules_notification_alert_type', () => {
references: [],
attributes: ruleAlert,
});
alertServices.callCluster.mockResolvedValue({
count: 0,
});
alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId());

await alert.executor(payload);

Expand All @@ -75,6 +79,7 @@ describe('rules_notification_alert_type', () => {
index: '.siem-signals',
ruleId: 'rule-1',
to: '1576341633400',
size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
})
);
});
Expand All @@ -88,9 +93,7 @@ describe('rules_notification_alert_type', () => {
references: [],
attributes: ruleAlert,
});
alertServices.callCluster.mockResolvedValue({
count: 10,
});
alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId());

await alert.executor(payload);
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();
Expand All @@ -114,9 +117,7 @@ describe('rules_notification_alert_type', () => {
references: [],
attributes: ruleAlert,
});
alertServices.callCluster.mockResolvedValue({
count: 10,
});
alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId());
await alert.executor(payload);
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();

Expand All @@ -141,9 +142,7 @@ describe('rules_notification_alert_type', () => {
references: [],
attributes: ruleAlert,
});
alertServices.callCluster.mockResolvedValue({
count: 10,
});
alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId());
await alert.executor(payload);
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();

Expand All @@ -165,9 +164,7 @@ describe('rules_notification_alert_type', () => {
references: [],
attributes: ruleAlert,
});
alertServices.callCluster.mockResolvedValue({
count: 0,
});
alertServices.callCluster.mockResolvedValue(sampleEmptyDocSearchResults());

await alert.executor(payload);

Expand All @@ -182,17 +179,15 @@ describe('rules_notification_alert_type', () => {
references: [],
attributes: ruleAlert,
});
alertServices.callCluster.mockResolvedValue({
count: 10,
});
alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortIdNoVersion());

await alert.executor(payload);

expect(alertServices.alertInstanceFactory).toHaveBeenCalled();

const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
expect(alertInstanceMock.replaceState).toHaveBeenCalledWith(
expect.objectContaining({ signals_count: 10 })
expect.objectContaining({ signals_count: 100 })
);
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith(
'default',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@

import { Logger } from 'src/core/server';
import { schema } from '@kbn/config-schema';
import { NOTIFICATIONS_ID, SERVER_APP_ID } from '../../../../common/constants';
import {
DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
NOTIFICATIONS_ID,
SERVER_APP_ID,
} from '../../../../common/constants';

import { NotificationAlertTypeDefinition } from './types';
import { getSignalsCount } from './get_signals_count';
import { RuleAlertAttributes } from '../signals/types';
import { siemRuleActionGroups } from '../signals/siem_rule_action_groups';
import { scheduleNotificationActions } from './schedule_notification_actions';
import { getNotificationResultsLink } from './utils';
import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates';
import { getSignals } from './get_signals';

export const rulesNotificationAlertType = ({
logger,
Expand Down Expand Up @@ -53,14 +57,20 @@ export const rulesNotificationAlertType = ({
)?.format('x');
const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x');

const signalsCount = await getSignalsCount({
const results = await getSignals({
from: fromInMs,
to: toInMs,
size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
index: ruleParams.outputIndex,
ruleId: ruleParams.ruleId,
callCluster: services.callCluster,
});

const signals = results.hits.hits.map((hit) => hit._source);

const signalsCount =
typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value;

const resultsLink = getNotificationResultsLink({
from: fromInMs,
to: toInMs,
Expand All @@ -75,7 +85,13 @@ export const rulesNotificationAlertType = ({

if (signalsCount !== 0) {
const alertInstance = services.alertInstanceFactory(alertId);
scheduleNotificationActions({ alertInstance, signalsCount, resultsLink, ruleParams });
scheduleNotificationActions({
alertInstance,
signalsCount,
resultsLink,
ruleParams,
signals,
});
}
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { mapKeys, snakeCase } from 'lodash/fp';
import { AlertInstance } from '../../../../../alerts/server';
import { SignalSource } from '../signals/types';
import { RuleTypeParams } from '../types';

export type NotificationRuleTypeParams = RuleTypeParams & {
Expand All @@ -18,13 +19,15 @@ interface ScheduleNotificationActions {
signalsCount: number;
resultsLink: string;
ruleParams: NotificationRuleTypeParams;
signals: SignalSource[];
}

export const scheduleNotificationActions = ({
alertInstance,
signalsCount,
resultsLink = '',
ruleParams,
signals,
}: ScheduleNotificationActions): AlertInstance =>
alertInstance
.replaceState({
Expand All @@ -33,4 +36,5 @@ export const scheduleNotificationActions = ({
.scheduleActions('default', {
results_link: resultsLink,
rule: mapKeys(snakeCase, ruleParams),
alerts: signals,
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
BulkItem,
RuleAlertAttributes,
SignalHit,
WrappedSignalHit,
} from '../types';
import {
Logger,
Expand Down Expand Up @@ -240,6 +241,14 @@ export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({
},
});

export const sampleWrappedSignalHit = (): WrappedSignalHit => {
return {
_index: 'myFakeSignalIndex',
_id: sampleIdGuid,
_source: sampleSignalHit(),
};
};

export const sampleDocWithAncestors = (): SignalSearchResponse => {
const sampleDoc = sampleDocNoSortId();
delete sampleDoc.sort;
Expand Down
Loading

0 comments on commit 818246e

Please sign in to comment.