Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detections] Add alert source to detection rule action context #85488

Merged
merged 6 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -52,14 +56,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 @@ -74,7 +84,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