[Security Solutions][Detection Engine] Removes side car actions object and side car notifications (Part 1) (#109722)
## Summary Removes the "side car" actions object and side car notification (Part 1). Part 1 makes it so that newly created rules and editing existing rules will update them to using the new side car notifications. Part 2 in a follow up PR will be the migrations to move the existing data. The saved object side we are removing usages of is: ``` siem-detection-engine-rule-actions ``` The alerting side car notification system we are removing is: ``` siem.notifications ``` * Removes the notification files and types * Adds transform to and from alerting concepts of `notityWhen` and our `throttle` * Adds unit tests for utilities and pure functions created * Updates unit tests to have more needed jest mock * Adds business rules and logic for the different states of `notifyWhen`, and `throttle` on each of the REST routes to determine when we should `muteAll` vs. not muting using secondary API call from client alerting * Adds e2e tests for the throttle conditions and how they are to interact with the kibana-alerting `throttle` and `notifyWhen` A behavioral change under the hood is that we now support the state changes of `muteAll` from the UI/UX of [stack management](https://www.elastic.co/guide/en/kibana/master/create-and-manage-rules.html#controlling-rules). Whenever the `security_solution` ["Perform no actions"](https://www.elastic.co/guide/en/security/current/rules-api-create.html ) is selected we do a `muteAll`. However, we do not change the state if all individual actions are muted within the rule. Instead we only maintain the state of `muteAll`: <img width="2299" alt="ui_state_change" src="https://user-images.githubusercontent.com/1151048/130823045-48a9f34b-db23-44e3-b9ed-cbbb57edc3d6.png"> <img width="1163" alt="no_actions_state_change" src="https://user-images.githubusercontent.com/1151048/130823056-3f8953fa-9433-4973-a2d3-6e11263b9619.png"> Ref: * Issue and PR where notifyWhen was added to kibna-alerting * https://github.com/elastic/kibana/pull/82969 * https://github.com/elastic/kibana/issues/50077 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
2348ced4c0
commit
ad01057f90
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* 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 { addTags } from './add_tags';
|
||||
import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants';
|
||||
|
||||
describe('add_tags', () => {
|
||||
test('it should add a rule id as an internal structure', () => {
|
||||
const tags = addTags([], 'rule-1');
|
||||
expect(tags).toEqual([`${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]);
|
||||
});
|
||||
|
||||
test('it should not allow duplicate tags to be created', () => {
|
||||
const tags = addTags(['tag-1', 'tag-1'], 'rule-1');
|
||||
expect(tags).toEqual(['tag-1', `${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]);
|
||||
});
|
||||
|
||||
test('it should not allow duplicate internal tags to be created when called two times in a row', () => {
|
||||
const tags1 = addTags(['tag-1'], 'rule-1');
|
||||
const tags2 = addTags(tags1, 'rule-1');
|
||||
expect(tags2).toEqual(['tag-1', `${INTERNAL_RULE_ALERT_ID_KEY}:rule-1`]);
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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 { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants';
|
||||
|
||||
export const addTags = (tags: string[], ruleAlertId: string): string[] =>
|
||||
Array.from(new Set([...tags, `${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}`]));
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* 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 { rulesClientMock } from '../../../../../alerting/server/mocks';
|
||||
import { createNotifications } from './create_notifications';
|
||||
|
||||
describe('createNotifications', () => {
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
|
||||
beforeEach(() => {
|
||||
rulesClient = rulesClientMock.create();
|
||||
});
|
||||
|
||||
it('calls the rulesClient with proper params', async () => {
|
||||
const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd';
|
||||
|
||||
await createNotifications({
|
||||
rulesClient,
|
||||
actions: [],
|
||||
ruleAlertId,
|
||||
enabled: true,
|
||||
interval: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
expect(rulesClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
ruleAlertId,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the rulesClient with transformed actions', async () => {
|
||||
const action = {
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
params: { message: 'Rule generated {{state.signals_count}} signals' },
|
||||
action_type_id: '.slack',
|
||||
};
|
||||
await createNotifications({
|
||||
rulesClient,
|
||||
actions: [action],
|
||||
ruleAlertId: 'new-rule-id',
|
||||
enabled: true,
|
||||
interval: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
expect(rulesClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
{
|
||||
group: action.group,
|
||||
id: action.id,
|
||||
params: action.params,
|
||||
actionTypeId: '.slack',
|
||||
},
|
||||
]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* 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 { SanitizedAlert } from '../../../../../alerting/common';
|
||||
import { SERVER_APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants';
|
||||
import { CreateNotificationParams, RuleNotificationAlertTypeParams } from './types';
|
||||
import { addTags } from './add_tags';
|
||||
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
|
||||
|
||||
export const createNotifications = async ({
|
||||
rulesClient,
|
||||
actions,
|
||||
enabled,
|
||||
ruleAlertId,
|
||||
interval,
|
||||
name,
|
||||
}: CreateNotificationParams): Promise<SanitizedAlert<RuleNotificationAlertTypeParams>> =>
|
||||
rulesClient.create<RuleNotificationAlertTypeParams>({
|
||||
data: {
|
||||
name,
|
||||
tags: addTags([], ruleAlertId),
|
||||
alertTypeId: NOTIFICATIONS_ID,
|
||||
consumer: SERVER_APP_ID,
|
||||
params: {
|
||||
ruleAlertId,
|
||||
},
|
||||
schedule: { interval },
|
||||
enabled,
|
||||
actions: actions.map(transformRuleToAlertAction),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
|
@ -1,142 +0,0 @@
|
|||
/*
|
||||
* 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 { rulesClientMock } from '../../../../../alerting/server/mocks';
|
||||
import { deleteNotifications } from './delete_notifications';
|
||||
import { readNotifications } from './read_notifications';
|
||||
jest.mock('./read_notifications');
|
||||
|
||||
describe('deleteNotifications', () => {
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
const notificationId = 'notification-52128c15-0d1b-4716-a4c5-46997ac7f3bd';
|
||||
const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd';
|
||||
|
||||
beforeEach(() => {
|
||||
rulesClient = rulesClientMock.create();
|
||||
});
|
||||
|
||||
it('should return null if notification was not found', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const result = await deleteNotifications({
|
||||
rulesClient,
|
||||
id: notificationId,
|
||||
ruleAlertId,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should call rulesClient.delete if notification was found', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue({
|
||||
id: notificationId,
|
||||
});
|
||||
|
||||
const result = await deleteNotifications({
|
||||
rulesClient,
|
||||
id: notificationId,
|
||||
ruleAlertId,
|
||||
});
|
||||
|
||||
expect(rulesClient.delete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: notificationId,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual({ id: notificationId });
|
||||
});
|
||||
|
||||
it('should call rulesClient.delete if notification.id was null', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue({
|
||||
id: null,
|
||||
});
|
||||
|
||||
const result = await deleteNotifications({
|
||||
rulesClient,
|
||||
id: notificationId,
|
||||
ruleAlertId,
|
||||
});
|
||||
|
||||
expect(rulesClient.delete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: notificationId,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual({ id: null });
|
||||
});
|
||||
|
||||
it('should return null if rulesClient.delete rejects with 404 if notification.id was null', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue({
|
||||
id: null,
|
||||
});
|
||||
|
||||
rulesClient.delete.mockRejectedValue({
|
||||
output: {
|
||||
statusCode: 404,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await deleteNotifications({
|
||||
rulesClient,
|
||||
id: notificationId,
|
||||
ruleAlertId,
|
||||
});
|
||||
|
||||
expect(rulesClient.delete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: notificationId,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return error object if rulesClient.delete rejects with status different than 404 and if notification.id was null', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue({
|
||||
id: null,
|
||||
});
|
||||
|
||||
const errorObject = {
|
||||
output: {
|
||||
statusCode: 500,
|
||||
},
|
||||
};
|
||||
|
||||
rulesClient.delete.mockRejectedValue(errorObject);
|
||||
|
||||
let errorResult;
|
||||
try {
|
||||
await deleteNotifications({
|
||||
rulesClient,
|
||||
id: notificationId,
|
||||
ruleAlertId,
|
||||
});
|
||||
} catch (error) {
|
||||
errorResult = error;
|
||||
}
|
||||
|
||||
expect(rulesClient.delete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: notificationId,
|
||||
})
|
||||
);
|
||||
expect(errorResult).toEqual(errorObject);
|
||||
});
|
||||
|
||||
it('should return null if notification.id and id were null', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue({
|
||||
id: null,
|
||||
});
|
||||
|
||||
const result = await deleteNotifications({
|
||||
rulesClient,
|
||||
id: undefined,
|
||||
ruleAlertId,
|
||||
});
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { readNotifications } from './read_notifications';
|
||||
import { DeleteNotificationParams } from './types';
|
||||
|
||||
export const deleteNotifications = async ({
|
||||
rulesClient,
|
||||
id,
|
||||
ruleAlertId,
|
||||
}: DeleteNotificationParams) => {
|
||||
const notification = await readNotifications({ rulesClient, id, ruleAlertId });
|
||||
if (notification == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (notification.id != null) {
|
||||
await rulesClient.delete({ id: notification.id });
|
||||
return notification;
|
||||
} else if (id != null) {
|
||||
try {
|
||||
await rulesClient.delete({ id });
|
||||
return notification;
|
||||
} catch (err) {
|
||||
if (err.output.statusCode === 404) {
|
||||
return null;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* 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 { getFilter } from './find_notifications';
|
||||
import { NOTIFICATIONS_ID } from '../../../../common/constants';
|
||||
|
||||
describe('find_notifications', () => {
|
||||
test('it returns a full filter with an AND if sent down', () => {
|
||||
expect(getFilter('alert.attributes.enabled: true')).toEqual(
|
||||
`alert.attributes.alertTypeId: ${NOTIFICATIONS_ID} AND alert.attributes.enabled: true`
|
||||
);
|
||||
});
|
||||
|
||||
test('it returns existing filter with no AND when not set', () => {
|
||||
expect(getFilter(null)).toEqual(`alert.attributes.alertTypeId: ${NOTIFICATIONS_ID}`);
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertTypeParams, FindResult } from '../../../../../alerting/server';
|
||||
import { NOTIFICATIONS_ID } from '../../../../common/constants';
|
||||
import { FindNotificationParams } from './types';
|
||||
|
||||
export const getFilter = (filter: string | null | undefined) => {
|
||||
if (filter == null) {
|
||||
return `alert.attributes.alertTypeId: ${NOTIFICATIONS_ID}`;
|
||||
} else {
|
||||
return `alert.attributes.alertTypeId: ${NOTIFICATIONS_ID} AND ${filter}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const findNotifications = async ({
|
||||
rulesClient,
|
||||
perPage,
|
||||
page,
|
||||
fields,
|
||||
filter,
|
||||
sortField,
|
||||
sortOrder,
|
||||
}: FindNotificationParams): Promise<FindResult<AlertTypeParams>> =>
|
||||
rulesClient.find({
|
||||
options: {
|
||||
fields,
|
||||
page,
|
||||
perPage,
|
||||
filter: getFilter(filter),
|
||||
sortOrder,
|
||||
sortField,
|
||||
},
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from 'kibana/server';
|
||||
import { buildSignalsSearchQuery } from './build_signals_query';
|
||||
|
||||
interface GetSignalsCount {
|
||||
from?: string;
|
||||
to?: string;
|
||||
ruleId: string;
|
||||
index: string;
|
||||
esClient: ElasticsearchClient;
|
||||
}
|
||||
|
||||
export const getSignalsCount = async ({
|
||||
from,
|
||||
to,
|
||||
ruleId,
|
||||
index,
|
||||
esClient,
|
||||
}: GetSignalsCount): Promise<number> => {
|
||||
if (from == null || to == null) {
|
||||
throw Error('"from" or "to" was not provided to signals count query');
|
||||
}
|
||||
|
||||
const query = buildSignalsSearchQuery({
|
||||
index,
|
||||
ruleId,
|
||||
to,
|
||||
from,
|
||||
});
|
||||
|
||||
const { body: result } = await esClient.count(query);
|
||||
|
||||
return result.count;
|
||||
};
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* 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 { readNotifications } from './read_notifications';
|
||||
import { rulesClientMock } from '../../../../../alerting/server/mocks';
|
||||
import {
|
||||
getNotificationResult,
|
||||
getFindNotificationsResultWithSingleHit,
|
||||
} from '../routes/__mocks__/request_responses';
|
||||
|
||||
class TestError extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = 'CustomError';
|
||||
this.output = { statusCode: 404 };
|
||||
}
|
||||
public output: { statusCode: number };
|
||||
}
|
||||
|
||||
describe('read_notifications', () => {
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
|
||||
beforeEach(() => {
|
||||
rulesClient = rulesClientMock.create();
|
||||
});
|
||||
|
||||
describe('readNotifications', () => {
|
||||
test('should return the output from rulesClient if id is set but ruleAlertId is undefined', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
ruleAlertId: undefined,
|
||||
});
|
||||
expect(rule).toEqual(getNotificationResult());
|
||||
});
|
||||
test('should return null if saved object found by alerts client given id is not alert type', async () => {
|
||||
const result = getNotificationResult();
|
||||
// @ts-expect-error
|
||||
delete result.alertTypeId;
|
||||
rulesClient.get.mockResolvedValue(result);
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
ruleAlertId: undefined,
|
||||
});
|
||||
expect(rule).toEqual(null);
|
||||
});
|
||||
|
||||
test('should return error if alerts client throws 404 error on get', async () => {
|
||||
rulesClient.get.mockImplementation(() => {
|
||||
throw new TestError();
|
||||
});
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
ruleAlertId: undefined,
|
||||
});
|
||||
expect(rule).toEqual(null);
|
||||
});
|
||||
|
||||
test('should return error if alerts client throws error on get', async () => {
|
||||
rulesClient.get.mockImplementation(() => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
try {
|
||||
await readNotifications({
|
||||
rulesClient,
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
ruleAlertId: undefined,
|
||||
});
|
||||
} catch (exc) {
|
||||
expect(exc.message).toEqual('Test error');
|
||||
}
|
||||
});
|
||||
|
||||
test('should return the output from rulesClient if id is set but ruleAlertId is null', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
ruleAlertId: null,
|
||||
});
|
||||
expect(rule).toEqual(getNotificationResult());
|
||||
});
|
||||
|
||||
test('should return the output from rulesClient if id is undefined but ruleAlertId is set', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit());
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: undefined,
|
||||
ruleAlertId: 'rule-1',
|
||||
});
|
||||
expect(rule).toEqual(getNotificationResult());
|
||||
});
|
||||
|
||||
test('should return null if the output from rulesClient with ruleAlertId set is empty', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
rulesClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 });
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: undefined,
|
||||
ruleAlertId: 'rule-1',
|
||||
});
|
||||
expect(rule).toEqual(null);
|
||||
});
|
||||
|
||||
test('should return the output from rulesClient if id is null but ruleAlertId is set', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit());
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: null,
|
||||
ruleAlertId: 'rule-1',
|
||||
});
|
||||
expect(rule).toEqual(getNotificationResult());
|
||||
});
|
||||
|
||||
test('should return null if id and ruleAlertId are null', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit());
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: null,
|
||||
ruleAlertId: null,
|
||||
});
|
||||
expect(rule).toEqual(null);
|
||||
});
|
||||
|
||||
test('should return null if id and ruleAlertId are undefined', async () => {
|
||||
rulesClient.get.mockResolvedValue(getNotificationResult());
|
||||
rulesClient.find.mockResolvedValue(getFindNotificationsResultWithSingleHit());
|
||||
|
||||
const rule = await readNotifications({
|
||||
rulesClient,
|
||||
id: undefined,
|
||||
ruleAlertId: undefined,
|
||||
});
|
||||
expect(rule).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertTypeParams, SanitizedAlert } from '../../../../../alerting/common';
|
||||
import { ReadNotificationParams, isAlertType } from './types';
|
||||
import { findNotifications } from './find_notifications';
|
||||
import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants';
|
||||
|
||||
export const readNotifications = async ({
|
||||
rulesClient,
|
||||
id,
|
||||
ruleAlertId,
|
||||
}: ReadNotificationParams): Promise<SanitizedAlert<AlertTypeParams> | null> => {
|
||||
if (id != null) {
|
||||
try {
|
||||
const notification = await rulesClient.get({ id });
|
||||
if (isAlertType(notification)) {
|
||||
return notification;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.output?.statusCode === 404) {
|
||||
return null;
|
||||
} else {
|
||||
// throw non-404 as they would be 500 or other internal errors
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else if (ruleAlertId != null) {
|
||||
const notificationFromFind = await findNotifications({
|
||||
rulesClient,
|
||||
filter: `alert.attributes.tags: "${INTERNAL_RULE_ALERT_ID_KEY}:${ruleAlertId}"`,
|
||||
page: 1,
|
||||
});
|
||||
if (notificationFromFind.data.length === 0 || !isAlertType(notificationFromFind.data[0])) {
|
||||
return null;
|
||||
} else {
|
||||
return notificationFromFind.data[0];
|
||||
}
|
||||
} else {
|
||||
// should never get here, and yet here we are.
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -1,247 +0,0 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { getAlertMock } from '../routes/__mocks__/request_responses';
|
||||
import { rulesNotificationAlertType } from './rules_notification_alert_type';
|
||||
import { buildSignalsSearchQuery } from './build_signals_query';
|
||||
import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks';
|
||||
import { NotificationExecutorOptions } from './types';
|
||||
import {
|
||||
sampleDocSearchResultsNoSortIdNoVersion,
|
||||
sampleDocSearchResultsWithSortId,
|
||||
sampleEmptyDocSearchResults,
|
||||
} from '../signals/__mocks__/es_results';
|
||||
import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
|
||||
jest.mock('./build_signals_query');
|
||||
|
||||
describe('rules_notification_alert_type', () => {
|
||||
let payload: NotificationExecutorOptions;
|
||||
let alert: ReturnType<typeof rulesNotificationAlertType>;
|
||||
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
let alertServices: AlertServicesMock;
|
||||
|
||||
beforeEach(() => {
|
||||
alertServices = alertsMock.createAlertServices();
|
||||
logger = loggingSystemMock.createLogger();
|
||||
|
||||
payload = {
|
||||
alertId: '1111',
|
||||
services: alertServices,
|
||||
params: { ruleAlertId: '2222' },
|
||||
state: {},
|
||||
spaceId: '',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
startedAt: new Date('2019-12-14T16:40:33.400Z'),
|
||||
previousStartedAt: new Date('2019-12-13T16:40:33.400Z'),
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
rule: {
|
||||
name: 'name',
|
||||
tags: [],
|
||||
consumer: 'foo',
|
||||
producer: 'foo',
|
||||
ruleTypeId: 'ruleType',
|
||||
ruleTypeName: 'Name of rule',
|
||||
enabled: true,
|
||||
schedule: {
|
||||
interval: '1h',
|
||||
},
|
||||
actions: [],
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2019-12-14T16:40:33.400Z'),
|
||||
updatedAt: new Date('2019-12-14T16:40:33.400Z'),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
},
|
||||
};
|
||||
|
||||
alert = rulesNotificationAlertType({
|
||||
logger,
|
||||
});
|
||||
});
|
||||
|
||||
describe('executor', () => {
|
||||
it('throws an error if rule alert was not found', async () => {
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'id',
|
||||
attributes: {},
|
||||
type: 'type',
|
||||
references: [],
|
||||
});
|
||||
await alert.executor(payload);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Saved object for alert ${payload.params.ruleAlertId} was not found`
|
||||
);
|
||||
});
|
||||
|
||||
it('should call buildSignalsSearchQuery with proper params', async () => {
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: ruleAlert,
|
||||
});
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
sampleDocSearchResultsWithSortId()
|
||||
)
|
||||
);
|
||||
|
||||
await alert.executor(payload);
|
||||
|
||||
expect(buildSignalsSearchQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
from: '1576255233400',
|
||||
index: '.siem-signals',
|
||||
ruleId: 'rule-1',
|
||||
to: '1576341633400',
|
||||
size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve results_link when meta is undefined to use "/app/security"', async () => {
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
delete ruleAlert.params.meta;
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'rule-id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: ruleAlert,
|
||||
});
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
sampleDocSearchResultsWithSortId()
|
||||
)
|
||||
);
|
||||
|
||||
await alert.executor(payload);
|
||||
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();
|
||||
|
||||
const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith(
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
results_link:
|
||||
'/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve results_link when meta is an empty object to use "/app/security"', async () => {
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
ruleAlert.params.meta = {};
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'rule-id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: ruleAlert,
|
||||
});
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
sampleDocSearchResultsWithSortId()
|
||||
)
|
||||
);
|
||||
await alert.executor(payload);
|
||||
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();
|
||||
|
||||
const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith(
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
results_link:
|
||||
'/app/security/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve results_link to custom kibana link when given one', async () => {
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
ruleAlert.params.meta = {
|
||||
kibana_siem_app_url: 'http://localhost',
|
||||
};
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'rule-id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: ruleAlert,
|
||||
});
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
sampleDocSearchResultsWithSortId()
|
||||
)
|
||||
);
|
||||
await alert.executor(payload);
|
||||
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();
|
||||
|
||||
const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith(
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
results_link:
|
||||
'http://localhost/detections/rules/id/rule-id?timerange=(global:(linkTo:!(timeline),timerange:(from:1576255233400,kind:absolute,to:1576341633400)),timeline:(linkTo:!(global),timerange:(from:1576255233400,kind:absolute,to:1576341633400)))',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call alertInstanceFactory if signalsCount was 0', async () => {
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: ruleAlert,
|
||||
});
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(sampleEmptyDocSearchResults())
|
||||
);
|
||||
|
||||
await alert.executor(payload);
|
||||
|
||||
expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call scheduleActions if signalsCount was greater than 0', async () => {
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
alertServices.savedObjectsClient.get.mockResolvedValue({
|
||||
id: 'id',
|
||||
type: 'type',
|
||||
references: [],
|
||||
attributes: ruleAlert,
|
||||
});
|
||||
alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise(
|
||||
sampleDocSearchResultsNoSortIdNoVersion()
|
||||
)
|
||||
);
|
||||
|
||||
await alert.executor(payload);
|
||||
|
||||
expect(alertServices.alertInstanceFactory).toHaveBeenCalled();
|
||||
|
||||
const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results;
|
||||
expect(alertInstanceMock.replaceState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signals_count: 100 })
|
||||
);
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith(
|
||||
'default',
|
||||
expect.objectContaining({
|
||||
rule: expect.objectContaining({
|
||||
name: ruleAlert.name,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* 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 { Logger } from 'src/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
|
||||
import {
|
||||
DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
|
||||
NOTIFICATIONS_ID,
|
||||
SERVER_APP_ID,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
import { NotificationAlertTypeDefinition } from './types';
|
||||
import { AlertAttributes } from '../signals/types';
|
||||
import { siemRuleActionGroups } from '../signals/siem_rule_action_groups';
|
||||
import { scheduleNotificationActions } from './schedule_notification_actions';
|
||||
import { getNotificationResultsLink } from './utils';
|
||||
import { getSignals } from './get_signals';
|
||||
|
||||
export const rulesNotificationAlertType = ({
|
||||
logger,
|
||||
}: {
|
||||
logger: Logger;
|
||||
}): NotificationAlertTypeDefinition => ({
|
||||
id: NOTIFICATIONS_ID,
|
||||
name: 'SIEM notification',
|
||||
actionGroups: siemRuleActionGroups,
|
||||
defaultActionGroupId: 'default',
|
||||
producer: SERVER_APP_ID,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
ruleAlertId: schema.string(),
|
||||
}),
|
||||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: false,
|
||||
async executor({ startedAt, previousStartedAt, alertId, services, params }) {
|
||||
const ruleAlertSavedObject = await services.savedObjectsClient.get<AlertAttributes>(
|
||||
'alert',
|
||||
params.ruleAlertId
|
||||
);
|
||||
|
||||
if (!ruleAlertSavedObject.attributes.params) {
|
||||
logger.error(`Saved object for alert ${params.ruleAlertId} was not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { params: ruleAlertParams, name: ruleName } = ruleAlertSavedObject.attributes;
|
||||
const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id };
|
||||
|
||||
const fromInMs = parseScheduleDates(
|
||||
previousStartedAt
|
||||
? previousStartedAt.toISOString()
|
||||
: `now-${ruleAlertSavedObject.attributes.schedule.interval}`
|
||||
)?.format('x');
|
||||
const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x');
|
||||
|
||||
const results = await getSignals({
|
||||
from: fromInMs,
|
||||
to: toInMs,
|
||||
size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
|
||||
index: ruleParams.outputIndex,
|
||||
ruleId: ruleParams.ruleId,
|
||||
esClient: services.scopedClusterClient.asCurrentUser,
|
||||
});
|
||||
|
||||
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,
|
||||
id: ruleAlertSavedObject.id,
|
||||
kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string } | undefined)
|
||||
?.kibana_siem_app_url,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Found ${signalsCount} signals using signal rule name: "${ruleParams.name}", id: "${params.ruleAlertId}", rule_id: "${ruleParams.ruleId}" in "${ruleParams.outputIndex}" index`
|
||||
);
|
||||
|
||||
if (signalsCount !== 0) {
|
||||
const alertInstance = services.alertInstanceFactory(alertId);
|
||||
scheduleNotificationActions({
|
||||
alertInstance,
|
||||
signalsCount,
|
||||
resultsLink,
|
||||
ruleParams,
|
||||
signals,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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 { elasticsearchServiceMock } from 'src/core/server/mocks';
|
||||
import { alertsMock } from '../../../../../alerting/server/mocks';
|
||||
import { scheduleThrottledNotificationActions } from './schedule_throttle_notification_actions';
|
||||
import {
|
||||
NotificationRuleTypeParams,
|
||||
scheduleNotificationActions,
|
||||
} from './schedule_notification_actions';
|
||||
|
||||
jest.mock('./schedule_notification_actions', () => ({
|
||||
scheduleNotificationActions: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('schedule_throttle_notification_actions', () => {
|
||||
let notificationRuleParams: NotificationRuleTypeParams;
|
||||
|
||||
beforeEach(() => {
|
||||
(scheduleNotificationActions as jest.Mock).mockReset();
|
||||
notificationRuleParams = {
|
||||
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'],
|
||||
note: '# sample markdown',
|
||||
version: 1,
|
||||
exceptionsList: [],
|
||||
};
|
||||
});
|
||||
|
||||
it('should call "scheduleNotificationActions" if the results length is 1 or greater', async () => {
|
||||
await scheduleThrottledNotificationActions({
|
||||
throttle: '1d',
|
||||
startedAt: new Date('2021-08-24T19:19:22.094Z'),
|
||||
id: '123',
|
||||
kibanaSiemAppUrl: 'http://www.example.com',
|
||||
outputIndex: 'output-123',
|
||||
ruleId: 'rule-123',
|
||||
esClient: elasticsearchServiceMock.createElasticsearchClient(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
})
|
||||
),
|
||||
alertInstance: alertsMock.createAlertInstanceFactory(),
|
||||
notificationRuleParams,
|
||||
});
|
||||
|
||||
expect(scheduleNotificationActions as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT call "scheduleNotificationActions" if the results length is 0', async () => {
|
||||
await scheduleThrottledNotificationActions({
|
||||
throttle: '1d',
|
||||
startedAt: new Date('2021-08-24T19:19:22.094Z'),
|
||||
id: '123',
|
||||
kibanaSiemAppUrl: 'http://www.example.com',
|
||||
outputIndex: 'output-123',
|
||||
ruleId: 'rule-123',
|
||||
esClient: elasticsearchServiceMock.createElasticsearchClient(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
hits: {
|
||||
hits: [],
|
||||
total: 0,
|
||||
},
|
||||
})
|
||||
),
|
||||
alertInstance: alertsMock.createAlertInstanceFactory(),
|
||||
notificationRuleParams,
|
||||
});
|
||||
|
||||
expect(scheduleNotificationActions as jest.Mock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT call "scheduleNotificationActions" if "throttle" is an invalid string', async () => {
|
||||
await scheduleThrottledNotificationActions({
|
||||
throttle: 'invalid',
|
||||
startedAt: new Date('2021-08-24T19:19:22.094Z'),
|
||||
id: '123',
|
||||
kibanaSiemAppUrl: 'http://www.example.com',
|
||||
outputIndex: 'output-123',
|
||||
ruleId: 'rule-123',
|
||||
esClient: elasticsearchServiceMock.createElasticsearchClient(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
})
|
||||
),
|
||||
alertInstance: alertsMock.createAlertInstanceFactory(),
|
||||
notificationRuleParams,
|
||||
});
|
||||
|
||||
expect(scheduleNotificationActions as jest.Mock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass expected arguments into "scheduleNotificationActions" on success', async () => {
|
||||
await scheduleThrottledNotificationActions({
|
||||
throttle: '1d',
|
||||
startedAt: new Date('2021-08-24T19:19:22.094Z'),
|
||||
id: '123',
|
||||
kibanaSiemAppUrl: 'http://www.example.com',
|
||||
outputIndex: 'output-123',
|
||||
ruleId: 'rule-123',
|
||||
esClient: elasticsearchServiceMock.createElasticsearchClient(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
test: 123,
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
})
|
||||
),
|
||||
alertInstance: alertsMock.createAlertInstanceFactory(),
|
||||
notificationRuleParams,
|
||||
});
|
||||
|
||||
expect((scheduleNotificationActions as jest.Mock).mock.calls[0][0].resultsLink).toMatch(
|
||||
'http://www.example.com/detections/rules/id/123'
|
||||
);
|
||||
expect(scheduleNotificationActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
signalsCount: 1,
|
||||
signals: [{ test: 123 }],
|
||||
ruleParams: notificationRuleParams,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient, SavedObject } from 'src/core/server';
|
||||
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { AlertInstance } from '../../../../../alerting/server';
|
||||
import { RuleParams } from '../schemas/rule_schemas';
|
||||
import { getNotificationResultsLink } from '../notifications/utils';
|
||||
import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants';
|
||||
import { getSignals } from '../notifications/get_signals';
|
||||
import {
|
||||
NotificationRuleTypeParams,
|
||||
scheduleNotificationActions,
|
||||
} from './schedule_notification_actions';
|
||||
import { AlertAttributes } from '../signals/types';
|
||||
|
||||
/**
|
||||
* Schedules a throttled notification action for executor rules.
|
||||
* @param throttle The throttle which is the alerting saved object throttle
|
||||
* @param startedAt When the executor started at
|
||||
* @param id The id the alert which caused the notifications
|
||||
* @param kibanaSiemAppUrl The security_solution application url
|
||||
* @param outputIndex The alerting index we wrote the signals into
|
||||
* @param ruleId The rule_id of the alert which caused the notifications
|
||||
* @param esClient The elastic client to do queries
|
||||
* @param alertInstance The alert instance for notifications
|
||||
* @param notificationRuleParams The notification rule parameters
|
||||
*/
|
||||
export const scheduleThrottledNotificationActions = async ({
|
||||
throttle,
|
||||
startedAt,
|
||||
id,
|
||||
kibanaSiemAppUrl,
|
||||
outputIndex,
|
||||
ruleId,
|
||||
esClient,
|
||||
alertInstance,
|
||||
notificationRuleParams,
|
||||
}: {
|
||||
id: SavedObject['id'];
|
||||
startedAt: Date;
|
||||
throttle: AlertAttributes['throttle'];
|
||||
kibanaSiemAppUrl: string | undefined;
|
||||
outputIndex: RuleParams['outputIndex'];
|
||||
ruleId: RuleParams['ruleId'];
|
||||
esClient: ElasticsearchClient;
|
||||
alertInstance: AlertInstance;
|
||||
notificationRuleParams: NotificationRuleTypeParams;
|
||||
}): Promise<void> => {
|
||||
const fromInMs = parseScheduleDates(`now-${throttle}`);
|
||||
const toInMs = parseScheduleDates(startedAt.toISOString());
|
||||
|
||||
if (fromInMs != null && toInMs != null) {
|
||||
const resultsLink = getNotificationResultsLink({
|
||||
from: fromInMs.toISOString(),
|
||||
to: toInMs.toISOString(),
|
||||
id,
|
||||
kibanaSiemAppUrl,
|
||||
});
|
||||
|
||||
const results = await getSignals({
|
||||
from: `${fromInMs.valueOf()}`,
|
||||
to: `${toInMs.valueOf()}`,
|
||||
size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE,
|
||||
index: outputIndex,
|
||||
ruleId,
|
||||
esClient,
|
||||
});
|
||||
|
||||
const signalsCount =
|
||||
typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value;
|
||||
|
||||
const signals = results.hits.hits.map((hit) => hit._source);
|
||||
if (results.hits.hits.length !== 0) {
|
||||
scheduleNotificationActions({
|
||||
alertInstance,
|
||||
signalsCount,
|
||||
signals,
|
||||
resultsLink,
|
||||
ruleParams: notificationRuleParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { getNotificationResult, getAlertMock } from '../routes/__mocks__/request_responses';
|
||||
import { isAlertTypes, isNotificationAlertExecutor } from './types';
|
||||
import { rulesNotificationAlertType } from './rules_notification_alert_type';
|
||||
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
|
||||
|
||||
describe('types', () => {
|
||||
it('isAlertTypes should return true if is RuleNotificationAlertType type', () => {
|
||||
expect(isAlertTypes([getNotificationResult()])).toEqual(true);
|
||||
});
|
||||
|
||||
it('isAlertTypes should return false if is not RuleNotificationAlertType', () => {
|
||||
expect(isAlertTypes([getAlertMock(getQueryRuleParams())])).toEqual(false);
|
||||
});
|
||||
|
||||
it('isNotificationAlertExecutor should return true it passed object is NotificationAlertTypeDefinition type', () => {
|
||||
expect(
|
||||
isNotificationAlertExecutor(
|
||||
rulesNotificationAlertType({ logger: loggingSystemMock.createLogger() })
|
||||
)
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
RulesClient,
|
||||
PartialAlert,
|
||||
AlertType,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
AlertExecutorOptions,
|
||||
} from '../../../../../alerting/server';
|
||||
import { Alert } from '../../../../../alerting/common';
|
||||
import { NOTIFICATIONS_ID } from '../../../../common/constants';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
|
||||
export interface RuleNotificationAlertTypeParams extends AlertTypeParams {
|
||||
ruleAlertId: string;
|
||||
}
|
||||
export type RuleNotificationAlertType = Alert<RuleNotificationAlertTypeParams>;
|
||||
|
||||
export interface FindNotificationParams {
|
||||
rulesClient: RulesClient;
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
sortField?: string;
|
||||
filter?: string;
|
||||
fields?: string[];
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface FindNotificationsRequestParams {
|
||||
per_page: number;
|
||||
page: number;
|
||||
search?: string;
|
||||
sort_field?: string;
|
||||
filter?: string;
|
||||
fields?: string[];
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface Clients {
|
||||
rulesClient: RulesClient;
|
||||
}
|
||||
|
||||
export type UpdateNotificationParams = Omit<
|
||||
NotificationAlertParams,
|
||||
'interval' | 'actions' | 'tags'
|
||||
> & {
|
||||
actions: RuleAlertAction[];
|
||||
interval: string | null | undefined;
|
||||
ruleAlertId: string;
|
||||
} & Clients;
|
||||
|
||||
export type DeleteNotificationParams = Clients & {
|
||||
id?: string;
|
||||
ruleAlertId?: string;
|
||||
};
|
||||
|
||||
export interface NotificationAlertParams {
|
||||
actions: RuleAlertAction[];
|
||||
enabled: boolean;
|
||||
ruleAlertId: string;
|
||||
interval: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type CreateNotificationParams = NotificationAlertParams & Clients;
|
||||
|
||||
export interface ReadNotificationParams {
|
||||
rulesClient: RulesClient;
|
||||
id?: string | null;
|
||||
ruleAlertId?: string | null;
|
||||
}
|
||||
|
||||
export const isAlertTypes = (
|
||||
partialAlert: Array<PartialAlert<AlertTypeParams>>
|
||||
): partialAlert is RuleNotificationAlertType[] => {
|
||||
return partialAlert.every((rule) => isAlertType(rule));
|
||||
};
|
||||
|
||||
export const isAlertType = (
|
||||
partialAlert: PartialAlert<AlertTypeParams>
|
||||
): partialAlert is RuleNotificationAlertType => {
|
||||
return partialAlert.alertTypeId === NOTIFICATIONS_ID;
|
||||
};
|
||||
|
||||
export type NotificationExecutorOptions = AlertExecutorOptions<
|
||||
RuleNotificationAlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext
|
||||
>;
|
||||
|
||||
// This returns true because by default a NotificationAlertTypeDefinition is an AlertType
|
||||
// since we are only increasing the strictness of params.
|
||||
export const isNotificationAlertExecutor = (
|
||||
obj: NotificationAlertTypeDefinition
|
||||
): obj is AlertType<
|
||||
AlertTypeParams,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext
|
||||
> => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export type NotificationAlertTypeDefinition = Omit<
|
||||
AlertType<
|
||||
AlertTypeParams,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
'default'
|
||||
>,
|
||||
'executor'
|
||||
> & {
|
||||
executor: ({
|
||||
services,
|
||||
params,
|
||||
state,
|
||||
}: NotificationExecutorOptions) => Promise<AlertTypeState | void>;
|
||||
};
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* 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 { rulesClientMock } from '../../../../../alerting/server/mocks';
|
||||
import { updateNotifications } from './update_notifications';
|
||||
import { readNotifications } from './read_notifications';
|
||||
import { createNotifications } from './create_notifications';
|
||||
import { getNotificationResult } from '../routes/__mocks__/request_responses';
|
||||
import { UpdateNotificationParams } from './types';
|
||||
jest.mock('./read_notifications');
|
||||
jest.mock('./create_notifications');
|
||||
|
||||
describe('updateNotifications', () => {
|
||||
const notification = getNotificationResult();
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
|
||||
beforeEach(() => {
|
||||
rulesClient = rulesClientMock.create();
|
||||
});
|
||||
|
||||
it('should update the existing notification if interval provided', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue(notification);
|
||||
|
||||
await updateNotifications({
|
||||
rulesClient,
|
||||
actions: [],
|
||||
ruleAlertId: 'new-rule-id',
|
||||
enabled: true,
|
||||
interval: '10m',
|
||||
name: '',
|
||||
});
|
||||
|
||||
expect(rulesClient.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: notification.id,
|
||||
data: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
ruleAlertId: 'new-rule-id',
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a new notification if did not exist', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const params: UpdateNotificationParams = {
|
||||
rulesClient,
|
||||
actions: [],
|
||||
ruleAlertId: 'new-rule-id',
|
||||
enabled: true,
|
||||
interval: '10m',
|
||||
name: '',
|
||||
};
|
||||
|
||||
await updateNotifications(params);
|
||||
|
||||
expect(createNotifications).toHaveBeenCalledWith(expect.objectContaining(params));
|
||||
});
|
||||
|
||||
it('should delete notification if notification was found and interval is null', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue(notification);
|
||||
|
||||
await updateNotifications({
|
||||
rulesClient,
|
||||
actions: [],
|
||||
ruleAlertId: 'new-rule-id',
|
||||
enabled: true,
|
||||
interval: null,
|
||||
name: '',
|
||||
});
|
||||
|
||||
expect(rulesClient.delete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: notification.id,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the rulesClient with transformed actions', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue(notification);
|
||||
const action = {
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
params: { message: 'Rule generated {{state.signals_count}} signals' },
|
||||
action_type_id: '.slack',
|
||||
};
|
||||
await updateNotifications({
|
||||
rulesClient,
|
||||
actions: [action],
|
||||
ruleAlertId: 'new-rule-id',
|
||||
enabled: true,
|
||||
interval: '10m',
|
||||
name: '',
|
||||
});
|
||||
|
||||
expect(rulesClient.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
{
|
||||
group: action.group,
|
||||
id: action.id,
|
||||
params: action.params,
|
||||
actionTypeId: '.slack',
|
||||
},
|
||||
]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null if notification was not found and interval was null', async () => {
|
||||
(readNotifications as jest.Mock).mockResolvedValue(null);
|
||||
const ruleAlertId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd';
|
||||
|
||||
const result = await updateNotifications({
|
||||
rulesClient,
|
||||
actions: [],
|
||||
enabled: true,
|
||||
ruleAlertId,
|
||||
name: notification.name,
|
||||
interval: null,
|
||||
});
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* 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 { PartialAlert } from '../../../../../alerting/server';
|
||||
import { readNotifications } from './read_notifications';
|
||||
import { RuleNotificationAlertTypeParams, UpdateNotificationParams } from './types';
|
||||
import { addTags } from './add_tags';
|
||||
import { createNotifications } from './create_notifications';
|
||||
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
|
||||
|
||||
export const updateNotifications = async ({
|
||||
rulesClient,
|
||||
actions,
|
||||
enabled,
|
||||
ruleAlertId,
|
||||
name,
|
||||
interval,
|
||||
}: UpdateNotificationParams): Promise<PartialAlert<RuleNotificationAlertTypeParams> | null> => {
|
||||
const notification = await readNotifications({ rulesClient, id: undefined, ruleAlertId });
|
||||
|
||||
if (interval && notification) {
|
||||
return rulesClient.update<RuleNotificationAlertTypeParams>({
|
||||
id: notification.id,
|
||||
data: {
|
||||
tags: addTags([], ruleAlertId),
|
||||
name,
|
||||
schedule: {
|
||||
interval,
|
||||
},
|
||||
actions: actions.map(transformRuleToAlertAction),
|
||||
params: {
|
||||
ruleAlertId,
|
||||
},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
},
|
||||
});
|
||||
} else if (interval && !notification) {
|
||||
return createNotifications({
|
||||
rulesClient,
|
||||
enabled,
|
||||
name,
|
||||
interval,
|
||||
actions,
|
||||
ruleAlertId,
|
||||
});
|
||||
} else if (!interval && notification) {
|
||||
await rulesClient.delete({ id: notification.id });
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -27,7 +27,6 @@ import {
|
|||
IRuleStatusSOAttributes,
|
||||
} from '../../rules/types';
|
||||
import { requestMock } from './request';
|
||||
import { RuleNotificationAlertType } from '../../notifications/types';
|
||||
import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema';
|
||||
import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema';
|
||||
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
|
@ -576,54 +575,6 @@ export const getSuccessfulSignalUpdateResponse = () => ({
|
|||
failures: [],
|
||||
});
|
||||
|
||||
export const getNotificationResult = (): RuleNotificationAlertType => ({
|
||||
id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba',
|
||||
name: 'Notification for Rule Test',
|
||||
tags: ['__internal_rule_alert_id:85b64e8a-2e40-4096-86af-5ac172c10825'],
|
||||
alertTypeId: 'siem.notifications',
|
||||
consumer: 'siem',
|
||||
params: {
|
||||
ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825',
|
||||
},
|
||||
schedule: {
|
||||
interval: '5m',
|
||||
},
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: '.slack',
|
||||
params: {
|
||||
message:
|
||||
'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}',
|
||||
},
|
||||
group: 'default',
|
||||
id: '99403909-ca9b-49ba-9d7a-7e5320e68d05',
|
||||
},
|
||||
],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: 'elastic',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: new Date('2020-03-21T11:15:13.530Z'),
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7',
|
||||
updatedAt: new Date('2020-03-21T12:37:08.730Z'),
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
});
|
||||
|
||||
export const getFindNotificationsResultWithSingleHit = (): FindHit<RuleNotificationAlertType> => ({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
data: [getNotificationResult()],
|
||||
});
|
||||
|
||||
export const getFinalizeSignalsMigrationRequest = () =>
|
||||
requestMock.create({
|
||||
method: 'post',
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
getEmptyFindResult,
|
||||
addPrepackagedRulesRequest,
|
||||
getFindResultWithSingleHit,
|
||||
getAlertMock,
|
||||
} from '../__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, createMockConfig, mockGetCurrentUser } from '../__mocks__';
|
||||
import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
|
||||
|
@ -21,6 +22,7 @@ import { ExceptionListClient } from '../../../../../../lists/server';
|
|||
import { installPrepackagedTimelines } from '../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
|
||||
jest.mock('../../rules/get_prepackaged_rules', () => {
|
||||
return {
|
||||
|
@ -90,6 +92,7 @@ describe('add_prepackaged_rules_route', () => {
|
|||
mockExceptionsClient = listMock.getExceptionListClient();
|
||||
|
||||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams()));
|
||||
|
||||
(installPrepackagedTimelines as jest.Mock).mockReset();
|
||||
(installPrepackagedTimelines as jest.Mock).mockResolvedValue({
|
||||
|
|
|
@ -11,7 +11,10 @@ import { createRuleValidateTypeDependents } from '../../../../../common/detectio
|
|||
import { createRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
|
||||
import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../types';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
DETECTION_ENGINE_RULES_URL,
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
} from '../../../../../common/constants';
|
||||
import { SetupPlugins } from '../../../../plugin';
|
||||
import { buildMlAuthz } from '../../../machine_learning/authz';
|
||||
import { throwHttpError } from '../../../machine_learning/validation';
|
||||
|
@ -21,7 +24,6 @@ import { transformValidateBulkError } from './validate';
|
|||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
|
||||
|
||||
import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters';
|
||||
|
||||
export const createRulesBulkRoute = (
|
||||
|
@ -103,21 +105,12 @@ export const createRulesBulkRoute = (
|
|||
data: internalRule,
|
||||
});
|
||||
|
||||
const ruleActions = await updateRulesNotifications({
|
||||
ruleAlertId: createdRule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: createdRule.enabled,
|
||||
actions: payloadRule.actions,
|
||||
throttle: payloadRule.throttle ?? null,
|
||||
name: createdRule.name,
|
||||
});
|
||||
// mutes if we are creating the rule with the explicit "no_actions"
|
||||
if (payloadRule.throttle === NOTIFICATION_THROTTLE_NO_ACTIONS) {
|
||||
await rulesClient.muteAll({ id: createdRule.id });
|
||||
}
|
||||
|
||||
return transformValidateBulkError(
|
||||
internalRule.params.ruleId,
|
||||
createdRule,
|
||||
ruleActions
|
||||
);
|
||||
return transformValidateBulkError(internalRule.params.ruleId, createdRule, undefined);
|
||||
} catch (err) {
|
||||
return transformBulkError(internalRule.params.ruleId, err);
|
||||
}
|
||||
|
|
|
@ -18,12 +18,10 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach
|
|||
import { buildMlAuthz } from '../../../machine_learning/authz';
|
||||
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
|
||||
import { createRulesRoute } from './create_rules_route';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
jest.mock('../../rules/update_rules_notifications');
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
|
||||
describe('create_rules', () => {
|
||||
|
@ -48,12 +46,6 @@ describe('create_rules', () => {
|
|||
|
||||
describe('status codes with actionClient and alertClient', () => {
|
||||
test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => {
|
||||
(updateRulesNotifications as jest.Mock).mockResolvedValue({
|
||||
id: 'id',
|
||||
actions: [],
|
||||
alertThrottle: null,
|
||||
ruleThrottle: 'no_actions',
|
||||
});
|
||||
const response = await server.inject(getCreateRequest(), context);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils';
|
||||
import { IRuleDataClient } from '../../../../../../rule_registry/server';
|
||||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
DETECTION_ENGINE_RULES_URL,
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
} from '../../../../../common/constants';
|
||||
import { SetupPlugins } from '../../../../plugin';
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../types';
|
||||
import { buildMlAuthz } from '../../../machine_learning/authz';
|
||||
|
@ -16,7 +19,6 @@ import { throwHttpError } from '../../../machine_learning/validation';
|
|||
import { readRules } from '../../rules/read_rules';
|
||||
import { buildSiemResponse } from '../utils';
|
||||
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request';
|
||||
import { newTransformValidate } from './validate';
|
||||
import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents';
|
||||
|
@ -95,22 +97,17 @@ export const createRulesRoute = (
|
|||
data: internalRule,
|
||||
});
|
||||
|
||||
const ruleActions = await updateRulesNotifications({
|
||||
ruleAlertId: createdRule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: createdRule.enabled,
|
||||
actions: request.body.actions,
|
||||
throttle: request.body.throttle ?? null,
|
||||
name: createdRule.name,
|
||||
});
|
||||
// mutes if we are creating the rule with the explicit "no_actions"
|
||||
if (request.body.throttle === NOTIFICATION_THROTTLE_NO_ACTIONS) {
|
||||
await rulesClient.muteAll({ id: createdRule.id });
|
||||
}
|
||||
|
||||
const ruleStatuses = await context.securitySolution.getExecutionLogClient().find({
|
||||
logsCount: 1,
|
||||
ruleId: createdRule.id,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
const [validated, errors] = newTransformValidate(createdRule, ruleActions, ruleStatuses[0]);
|
||||
const [validated, errors] = newTransformValidate(createdRule, ruleStatuses[0]);
|
||||
if (errors != null) {
|
||||
return siemResponse.error({ statusCode: 500, body: errors });
|
||||
} else {
|
||||
|
|
|
@ -50,7 +50,6 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
const rulesClient = context.alerting?.getRulesClient();
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
if (!rulesClient) {
|
||||
return siemResponse.error({ statusCode: 404 });
|
||||
|
@ -84,12 +83,11 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => {
|
|||
});
|
||||
await deleteRules({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
ruleStatusClient,
|
||||
ruleStatuses,
|
||||
id: rule.id,
|
||||
});
|
||||
return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses);
|
||||
return transformValidateBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses);
|
||||
} catch (err) {
|
||||
return transformBulkError(idOrRuleIdOrUnknown, err);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ export const deleteRulesRoute = (
|
|||
const { id, rule_id: ruleId } = request.query;
|
||||
|
||||
const rulesClient = context.alerting?.getRulesClient();
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
if (!rulesClient) {
|
||||
return siemResponse.error({ statusCode: 404 });
|
||||
|
@ -71,12 +70,11 @@ export const deleteRulesRoute = (
|
|||
});
|
||||
await deleteRules({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
ruleStatusClient,
|
||||
ruleStatuses,
|
||||
id: rule.id,
|
||||
});
|
||||
const transformed = transform(rule, undefined, ruleStatuses[0]);
|
||||
const transformed = transform(rule, ruleStatuses[0]);
|
||||
if (transformed == null) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' });
|
||||
} else {
|
||||
|
|
|
@ -18,7 +18,6 @@ import { findRules } from '../../rules/find_rules';
|
|||
import { buildSiemResponse } from '../utils';
|
||||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
|
||||
import { transformFindAlerts } from './utils';
|
||||
import { getBulkRuleActionsSavedObject } from '../../rule_actions/get_bulk_rule_actions_saved_object';
|
||||
|
||||
export const findRulesRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
|
@ -46,7 +45,6 @@ export const findRulesRoute = (
|
|||
try {
|
||||
const { query } = request;
|
||||
const rulesClient = context.alerting?.getRulesClient();
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
if (!rulesClient) {
|
||||
return siemResponse.error({ statusCode: 404 });
|
||||
|
@ -64,15 +62,12 @@ export const findRulesRoute = (
|
|||
});
|
||||
const alertIds = rules.data.map((rule) => rule.id);
|
||||
|
||||
const [ruleStatuses, ruleActions] = await Promise.all([
|
||||
execLogClient.findBulk({
|
||||
ruleIds: alertIds,
|
||||
logsCount: 1,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
}),
|
||||
getBulkRuleActionsSavedObject({ alertIds, savedObjectsClient }),
|
||||
]);
|
||||
const transformed = transformFindAlerts(rules, ruleActions, ruleStatuses);
|
||||
const ruleStatuses = await execLogClient.findBulk({
|
||||
ruleIds: alertIds,
|
||||
logsCount: 1,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
const transformed = transformFindAlerts(rules, ruleStatuses);
|
||||
if (transformed == null) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' });
|
||||
} else {
|
||||
|
|
|
@ -45,7 +45,7 @@ describe('import_rules_route', () => {
|
|||
ml = mlServicesMock.createSetupContract();
|
||||
|
||||
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules
|
||||
|
||||
clients.rulesClient.update.mockResolvedValue(getAlertMock(getQueryRuleParams()));
|
||||
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } })
|
||||
);
|
||||
|
|
|
@ -186,6 +186,7 @@ export const importRulesRoute = (
|
|||
note,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
throttle,
|
||||
version,
|
||||
exceptions_list: exceptionsList,
|
||||
} = parsedRule;
|
||||
|
@ -235,6 +236,7 @@ export const importRulesRoute = (
|
|||
severity,
|
||||
severityMapping,
|
||||
tags,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
|
@ -288,6 +290,7 @@ export const importRulesRoute = (
|
|||
severityMapping,
|
||||
tags,
|
||||
timestampOverride,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
|
|
|
@ -22,7 +22,6 @@ import { transformBulkError, buildSiemResponse } from '../utils';
|
|||
import { getIdBulkError } from './utils';
|
||||
import { transformValidateBulkError } from './validate';
|
||||
import { patchRules } from '../../rules/patch_rules';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
import { PartialFilter } from '../../types';
|
||||
|
||||
|
@ -168,6 +167,7 @@ export const patchRulesBulkRoute = (
|
|||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
throttle,
|
||||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
timestampOverride,
|
||||
|
@ -180,21 +180,12 @@ export const patchRulesBulkRoute = (
|
|||
exceptionsList,
|
||||
});
|
||||
if (rule != null && rule.enabled != null && rule.name != null) {
|
||||
const ruleActions = await updateRulesNotifications({
|
||||
ruleAlertId: rule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: rule.enabled,
|
||||
actions,
|
||||
throttle,
|
||||
name: rule.name,
|
||||
});
|
||||
const ruleStatuses = await ruleStatusClient.find({
|
||||
logsCount: 1,
|
||||
ruleId: rule.id,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses);
|
||||
return transformValidateBulkError(rule.id, rule, ruleStatuses);
|
||||
} else {
|
||||
return getIdBulkError({ id, ruleId });
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import { buildSiemResponse } from '../utils';
|
|||
|
||||
import { getIdError } from './utils';
|
||||
import { transformValidate } from './validate';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
import { PartialFilter } from '../../types';
|
||||
|
||||
|
@ -171,6 +170,7 @@ export const patchRulesRoute = (
|
|||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
throttle,
|
||||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
timestampOverride,
|
||||
|
@ -183,22 +183,13 @@ export const patchRulesRoute = (
|
|||
exceptionsList,
|
||||
});
|
||||
if (rule != null && rule.enabled != null && rule.name != null) {
|
||||
const ruleActions = await updateRulesNotifications({
|
||||
ruleAlertId: rule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: rule.enabled,
|
||||
actions,
|
||||
throttle,
|
||||
name: rule.name,
|
||||
});
|
||||
const ruleStatuses = await ruleStatusClient.find({
|
||||
logsCount: 1,
|
||||
ruleId: rule.id,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
|
||||
const [validated, errors] = transformValidate(rule, ruleActions, ruleStatuses[0]);
|
||||
const [validated, errors] = transformValidate(rule, ruleStatuses[0]);
|
||||
if (errors != null) {
|
||||
return siemResponse.error({ statusCode: 500, body: errors });
|
||||
} else {
|
||||
|
|
|
@ -19,8 +19,6 @@ import { duplicateRule } from '../../rules/duplicate_rule';
|
|||
import { enableRule } from '../../rules/enable_rule';
|
||||
import { findRules } from '../../rules/find_rules';
|
||||
import { getExportByObjectIds } from '../../rules/get_export_by_object_ids';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object';
|
||||
import { buildSiemResponse } from '../utils';
|
||||
|
||||
const BULK_ACTION_RULES_LIMIT = 10000;
|
||||
|
@ -112,7 +110,6 @@ export const performBulkActionRoute = (
|
|||
});
|
||||
await deleteRules({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
ruleStatusClient,
|
||||
ruleStatuses,
|
||||
id: rule.id,
|
||||
|
@ -125,24 +122,9 @@ export const performBulkActionRoute = (
|
|||
rules.data.map(async (rule) => {
|
||||
throwHttpError(await mlAuthz.validateRuleType(rule.params.type));
|
||||
|
||||
const createdRule = await rulesClient.create({
|
||||
await rulesClient.create({
|
||||
data: duplicateRule(rule),
|
||||
});
|
||||
|
||||
const ruleActions = await getRuleActionsSavedObject({
|
||||
savedObjectsClient,
|
||||
ruleAlertId: rule.id,
|
||||
});
|
||||
|
||||
await updateRulesNotifications({
|
||||
ruleAlertId: createdRule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: createdRule.enabled,
|
||||
actions: ruleActions?.actions || [],
|
||||
throttle: ruleActions?.alertThrottle,
|
||||
name: createdRule.name,
|
||||
});
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -19,7 +19,6 @@ import { getIdError, transform } from './utils';
|
|||
import { buildSiemResponse } from '../utils';
|
||||
|
||||
import { readRules } from '../../rules/read_rules';
|
||||
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
export const readRulesRoute = (
|
||||
|
@ -48,7 +47,6 @@ export const readRulesRoute = (
|
|||
const { id, rule_id: ruleId } = request.query;
|
||||
|
||||
const rulesClient = context.alerting?.getRulesClient();
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
|
||||
try {
|
||||
if (!rulesClient) {
|
||||
|
@ -62,10 +60,6 @@ export const readRulesRoute = (
|
|||
ruleId,
|
||||
});
|
||||
if (rule != null) {
|
||||
const ruleActions = await getRuleActionsSavedObject({
|
||||
savedObjectsClient,
|
||||
ruleAlertId: rule.id,
|
||||
});
|
||||
const ruleStatuses = await ruleStatusClient.find({
|
||||
logsCount: 1,
|
||||
ruleId: rule.id,
|
||||
|
@ -78,7 +72,7 @@ export const readRulesRoute = (
|
|||
currentStatus.attributes.statusDate = rule.executionStatus.lastExecutionDate.toISOString();
|
||||
currentStatus.attributes.status = RuleExecutionStatus.failed;
|
||||
}
|
||||
const transformed = transform(rule, ruleActions, currentStatus);
|
||||
const transformed = transform(rule, currentStatus);
|
||||
if (transformed == null) {
|
||||
return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' });
|
||||
} else {
|
||||
|
|
|
@ -19,7 +19,6 @@ import { getIdBulkError } from './utils';
|
|||
import { transformValidateBulkError } from './validate';
|
||||
import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
|
||||
export const updateRulesBulkRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
|
@ -77,21 +76,12 @@ export const updateRulesBulkRoute = (
|
|||
ruleUpdate: payloadRule,
|
||||
});
|
||||
if (rule != null) {
|
||||
const ruleActions = await updateRulesNotifications({
|
||||
ruleAlertId: rule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: payloadRule.enabled ?? true,
|
||||
actions: payloadRule.actions,
|
||||
throttle: payloadRule.throttle,
|
||||
name: payloadRule.name,
|
||||
});
|
||||
const ruleStatuses = await ruleStatusClient.find({
|
||||
logsCount: 1,
|
||||
ruleId: rule.id,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses);
|
||||
return transformValidateBulkError(rule.id, rule, ruleStatuses);
|
||||
} else {
|
||||
return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id });
|
||||
}
|
||||
|
|
|
@ -17,13 +17,11 @@ import {
|
|||
} from '../__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { updateRulesRoute } from './update_rules_route';
|
||||
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
|
||||
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
|
||||
jest.mock('../../rules/update_rules_notifications');
|
||||
|
||||
describe('update_rules', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
|
@ -45,12 +43,6 @@ describe('update_rules', () => {
|
|||
|
||||
describe('status codes with actionClient and alertClient', () => {
|
||||
test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => {
|
||||
(updateRulesNotifications as jest.Mock).mockResolvedValue({
|
||||
id: 'id',
|
||||
actions: [],
|
||||
alertThrottle: null,
|
||||
ruleThrottle: 'no_actions',
|
||||
});
|
||||
const response = await server.inject(getUpdateRequest(), context);
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
|
|
@ -19,7 +19,6 @@ import { buildSiemResponse } from '../utils';
|
|||
import { getIdError } from './utils';
|
||||
import { transformValidate } from './validate';
|
||||
import { updateRules } from '../../rules/update_rules';
|
||||
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
|
||||
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
|
||||
|
||||
export const updateRulesRoute = (
|
||||
|
@ -70,21 +69,12 @@ export const updateRulesRoute = (
|
|||
});
|
||||
|
||||
if (rule != null) {
|
||||
const ruleActions = await updateRulesNotifications({
|
||||
ruleAlertId: rule.id,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
enabled: request.body.enabled ?? true,
|
||||
actions: request.body.actions ?? [],
|
||||
throttle: request.body.throttle ?? 'no_actions',
|
||||
name: request.body.name,
|
||||
});
|
||||
const ruleStatuses = await ruleStatusClient.find({
|
||||
logsCount: 1,
|
||||
ruleId: rule.id,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
});
|
||||
const [validated, errors] = transformValidate(rule, ruleActions, ruleStatuses[0]);
|
||||
const [validated, errors] = transformValidate(rule, ruleStatuses[0]);
|
||||
if (errors != null) {
|
||||
return siemResponse.error({ statusCode: 500, body: errors });
|
||||
} else {
|
||||
|
|
|
@ -255,7 +255,7 @@ describe('utils', () => {
|
|||
|
||||
describe('transformFindAlerts', () => {
|
||||
test('outputs empty data set when data set is empty correct', () => {
|
||||
const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, {}, {});
|
||||
const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, {});
|
||||
expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 });
|
||||
});
|
||||
|
||||
|
@ -267,7 +267,6 @@ describe('utils', () => {
|
|||
total: 0,
|
||||
data: [getAlertMock(getQueryRuleParams())],
|
||||
},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
const expected = getOutputRuleAlertForRest();
|
||||
|
|
|
@ -30,7 +30,6 @@ import {
|
|||
createImportErrorObject,
|
||||
OutputError,
|
||||
} from '../utils';
|
||||
import { RuleActions } from '../../rule_actions/types';
|
||||
import { internalRuleToAPIResponse } from '../../schemas/rule_converters';
|
||||
import { RuleParams } from '../../schemas/rule_schemas';
|
||||
import { SanitizedAlert } from '../../../../../../alerting/common';
|
||||
|
@ -104,10 +103,9 @@ export const transformTags = (tags: string[]): string[] => {
|
|||
// those on the export
|
||||
export const transformAlertToRule = (
|
||||
alert: SanitizedAlert<RuleParams>,
|
||||
ruleActions?: RuleActions | null,
|
||||
ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes>
|
||||
): Partial<RulesSchema> => {
|
||||
return internalRuleToAPIResponse(alert, ruleActions, ruleStatus?.attributes);
|
||||
return internalRuleToAPIResponse(alert, ruleStatus?.attributes);
|
||||
};
|
||||
|
||||
export const transformAlertsToRules = (alerts: RuleAlertType[]): Array<Partial<RulesSchema>> => {
|
||||
|
@ -116,7 +114,6 @@ export const transformAlertsToRules = (alerts: RuleAlertType[]): Array<Partial<R
|
|||
|
||||
export const transformFindAlerts = (
|
||||
findResults: FindResult<RuleParams>,
|
||||
ruleActions: { [key: string]: RuleActions | undefined },
|
||||
ruleStatuses: { [key: string]: IRuleStatusSOAttributes[] | undefined }
|
||||
): {
|
||||
page: number;
|
||||
|
@ -131,20 +128,18 @@ export const transformFindAlerts = (
|
|||
data: findResults.data.map((alert) => {
|
||||
const statuses = ruleStatuses[alert.id];
|
||||
const status = statuses ? statuses[0] : undefined;
|
||||
return internalRuleToAPIResponse(alert, ruleActions[alert.id], status);
|
||||
return internalRuleToAPIResponse(alert, status);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const transform = (
|
||||
alert: PartialAlert<RuleParams>,
|
||||
ruleActions?: RuleActions | null,
|
||||
ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes>
|
||||
): Partial<RulesSchema> | null => {
|
||||
if (isAlertType(alert)) {
|
||||
return transformAlertToRule(
|
||||
alert,
|
||||
ruleActions,
|
||||
isRuleStatusSavedObjectType(ruleStatus) ? ruleStatus : undefined
|
||||
);
|
||||
}
|
||||
|
@ -155,14 +150,13 @@ export const transform = (
|
|||
export const transformOrBulkError = (
|
||||
ruleId: string,
|
||||
alert: PartialAlert<RuleParams>,
|
||||
ruleActions: RuleActions,
|
||||
ruleStatus?: unknown
|
||||
): Partial<RulesSchema> | BulkError => {
|
||||
if (isAlertType(alert)) {
|
||||
if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) {
|
||||
return transformAlertToRule(alert, ruleActions, ruleStatus?.saved_objects[0] ?? ruleStatus);
|
||||
return transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus);
|
||||
} else {
|
||||
return transformAlertToRule(alert, ruleActions);
|
||||
return transformAlertToRule(alert);
|
||||
}
|
||||
} else {
|
||||
return createBulkErrorObject({
|
||||
|
|
|
@ -110,7 +110,7 @@ describe('validate', () => {
|
|||
test('it should do a validation correctly of a rule id with ruleStatus passed in', () => {
|
||||
const ruleStatuses = getRuleExecutionStatuses();
|
||||
const ruleAlert = getAlertMock(getQueryRuleParams());
|
||||
const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatuses);
|
||||
const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, ruleStatuses);
|
||||
const expected: RulesSchema = {
|
||||
...ruleOutput(),
|
||||
status: RuleExecutionStatus.succeeded,
|
||||
|
|
|
@ -25,15 +25,13 @@ import {
|
|||
} from '../../rules/types';
|
||||
import { createBulkErrorObject, BulkError } from '../utils';
|
||||
import { transform, transformAlertToRule } from './utils';
|
||||
import { RuleActions } from '../../rule_actions/types';
|
||||
import { RuleParams } from '../../schemas/rule_schemas';
|
||||
|
||||
export const transformValidate = (
|
||||
alert: PartialAlert<RuleParams>,
|
||||
ruleActions?: RuleActions | null,
|
||||
ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes>
|
||||
): [RulesSchema | null, string | null] => {
|
||||
const transformed = transform(alert, ruleActions, ruleStatus);
|
||||
const transformed = transform(alert, ruleStatus);
|
||||
if (transformed == null) {
|
||||
return [null, 'Internal error transforming'];
|
||||
} else {
|
||||
|
@ -43,10 +41,9 @@ export const transformValidate = (
|
|||
|
||||
export const newTransformValidate = (
|
||||
alert: PartialAlert<RuleParams>,
|
||||
ruleActions?: RuleActions | null,
|
||||
ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes>
|
||||
): [FullResponseSchema | null, string | null] => {
|
||||
const transformed = transform(alert, ruleActions, ruleStatus);
|
||||
const transformed = transform(alert, ruleStatus);
|
||||
if (transformed == null) {
|
||||
return [null, 'Internal error transforming'];
|
||||
} else {
|
||||
|
@ -57,12 +54,11 @@ export const newTransformValidate = (
|
|||
export const transformValidateBulkError = (
|
||||
ruleId: string,
|
||||
alert: PartialAlert<RuleParams>,
|
||||
ruleActions?: RuleActions | null,
|
||||
ruleStatus?: Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>
|
||||
): RulesSchema | BulkError => {
|
||||
if (isAlertType(alert)) {
|
||||
if (ruleStatus && ruleStatus?.length > 0 && isRuleStatusSavedObjectType(ruleStatus[0])) {
|
||||
const transformed = transformAlertToRule(alert, ruleActions, ruleStatus[0]);
|
||||
const transformed = transformAlertToRule(alert, ruleStatus[0]);
|
||||
const [validated, errors] = validateNonExact(transformed, rulesSchema);
|
||||
if (errors != null || validated == null) {
|
||||
return createBulkErrorObject({
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* 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 { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { AlertServices } from '../../../../../alerting/server';
|
||||
import { ruleActionsSavedObjectType } from './saved_object_mappings';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes } from './types';
|
||||
import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils';
|
||||
import { RulesActionsSavedObject } from './get_rule_actions_saved_object';
|
||||
|
||||
interface CreateRuleActionsSavedObject {
|
||||
ruleAlertId: string;
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
actions: RuleAlertAction[] | undefined;
|
||||
throttle: string | null | undefined;
|
||||
}
|
||||
|
||||
export const createRuleActionsSavedObject = async ({
|
||||
ruleAlertId,
|
||||
savedObjectsClient,
|
||||
actions = [],
|
||||
throttle,
|
||||
}: CreateRuleActionsSavedObject): Promise<RulesActionsSavedObject> => {
|
||||
const ruleActionsSavedObject = await savedObjectsClient.create<IRuleActionsAttributesSavedObjectAttributes>(
|
||||
ruleActionsSavedObjectType,
|
||||
{
|
||||
ruleAlertId,
|
||||
actions,
|
||||
...getThrottleOptions(throttle),
|
||||
}
|
||||
);
|
||||
|
||||
return getRuleActionsFromSavedObject(ruleActionsSavedObject);
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertServices } from '../../../../../alerting/server';
|
||||
import { ruleActionsSavedObjectType } from './saved_object_mappings';
|
||||
import { getRuleActionsSavedObject } from './get_rule_actions_saved_object';
|
||||
|
||||
interface DeleteRuleActionsSavedObject {
|
||||
ruleAlertId: string;
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
}
|
||||
|
||||
export const deleteRuleActionsSavedObject = async ({
|
||||
ruleAlertId,
|
||||
savedObjectsClient,
|
||||
}: DeleteRuleActionsSavedObject): Promise<{} | null> => {
|
||||
const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient });
|
||||
if (ruleActions != null) {
|
||||
return savedObjectsClient.delete(ruleActionsSavedObjectType, ruleActions.id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertServices } from '../../../../../alerting/server';
|
||||
import { ruleActionsSavedObjectType } from './saved_object_mappings';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes } from './types';
|
||||
import { getRuleActionsFromSavedObject } from './utils';
|
||||
import { RulesActionsSavedObject } from './get_rule_actions_saved_object';
|
||||
import { buildChunkedOrFilter } from '../signals/utils';
|
||||
|
||||
interface GetBulkRuleActionsSavedObject {
|
||||
alertIds: string[];
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
}
|
||||
|
||||
export const getBulkRuleActionsSavedObject = async ({
|
||||
alertIds,
|
||||
savedObjectsClient,
|
||||
}: GetBulkRuleActionsSavedObject): Promise<Record<string, RulesActionsSavedObject>> => {
|
||||
const filter = buildChunkedOrFilter(
|
||||
`${ruleActionsSavedObjectType}.attributes.ruleAlertId`,
|
||||
alertIds
|
||||
);
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
saved_objects,
|
||||
} = await savedObjectsClient.find<IRuleActionsAttributesSavedObjectAttributes>({
|
||||
type: ruleActionsSavedObjectType,
|
||||
perPage: 10000,
|
||||
filter,
|
||||
});
|
||||
return saved_objects.reduce((acc: { [key: string]: RulesActionsSavedObject }, savedObject) => {
|
||||
acc[savedObject.attributes.ruleAlertId] = getRuleActionsFromSavedObject(savedObject);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* 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 { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { AlertServices } from '../../../../../alerting/server';
|
||||
import { ruleActionsSavedObjectType } from './saved_object_mappings';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes } from './types';
|
||||
import { getRuleActionsFromSavedObject } from './utils';
|
||||
|
||||
interface GetRuleActionsSavedObject {
|
||||
ruleAlertId: string;
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
}
|
||||
|
||||
export interface RulesActionsSavedObject {
|
||||
id: string;
|
||||
actions: RuleAlertAction[];
|
||||
alertThrottle: string | null;
|
||||
ruleThrottle: string;
|
||||
}
|
||||
|
||||
export const getRuleActionsSavedObject = async ({
|
||||
ruleAlertId,
|
||||
savedObjectsClient,
|
||||
}: GetRuleActionsSavedObject): Promise<RulesActionsSavedObject | null> => {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
saved_objects,
|
||||
} = await savedObjectsClient.find<IRuleActionsAttributesSavedObjectAttributes>({
|
||||
type: ruleActionsSavedObjectType,
|
||||
perPage: 1,
|
||||
search: `${ruleAlertId}`,
|
||||
searchFields: ['ruleAlertId'],
|
||||
});
|
||||
|
||||
if (!saved_objects[0]) {
|
||||
return null;
|
||||
} else {
|
||||
return getRuleActionsFromSavedObject(saved_objects[0]);
|
||||
}
|
||||
};
|
|
@ -5,13 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import {
|
||||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectSanitizedDoc,
|
||||
SavedObjectAttributes,
|
||||
} from '../../../../../../../src/core/server';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes, RuleAlertAction } from './types';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes } from './types';
|
||||
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations
|
||||
* @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0)
|
||||
*/
|
||||
function isEmptyObject(obj: {}) {
|
||||
for (const attr in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, attr)) {
|
||||
|
@ -21,6 +28,12 @@ function isEmptyObject(obj: {}) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations
|
||||
* @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0)
|
||||
*/
|
||||
export const ruleActionsSavedObjectMigration = {
|
||||
'7.11.2': (
|
||||
doc: SavedObjectUnsanitizedDoc<IRuleActionsAttributesSavedObjectAttributes>
|
||||
|
|
|
@ -8,9 +8,21 @@
|
|||
import { SavedObjectsType } from '../../../../../../../src/core/server';
|
||||
import { ruleActionsSavedObjectMigration } from './migrations';
|
||||
|
||||
export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions';
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations.
|
||||
* * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0)
|
||||
*/
|
||||
const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions';
|
||||
|
||||
export const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = {
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations.
|
||||
* * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0)
|
||||
*/
|
||||
const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = {
|
||||
properties: {
|
||||
alertThrottle: {
|
||||
type: 'keyword',
|
||||
|
@ -41,6 +53,12 @@ export const ruleActionsSavedObjectMappings: SavedObjectsType['mappings'] = {
|
|||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations.
|
||||
* @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0)
|
||||
*/
|
||||
export const type: SavedObjectsType = {
|
||||
name: ruleActionsSavedObjectType,
|
||||
hidden: false,
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { SavedObjectAttributes } from 'kibana/server';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
|
||||
export { RuleAlertAction };
|
||||
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations.
|
||||
* @deprecated
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface IRuleActionsAttributes extends Record<string, any> {
|
||||
ruleAlertId: string;
|
||||
|
@ -19,53 +22,12 @@ export interface IRuleActionsAttributes extends Record<string, any> {
|
|||
alertThrottle: string | null;
|
||||
}
|
||||
|
||||
export interface RuleActions {
|
||||
id: string;
|
||||
actions: RuleAlertAction[];
|
||||
ruleThrottle: string;
|
||||
alertThrottle: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* We keep this around to migrate and update data for the old deprecated rule actions saved object mapping but we
|
||||
* do not use it anymore within the code base. Once we feel comfortable that users are upgrade far enough and this is no longer
|
||||
* needed then it will be safe to remove this saved object and all its migrations.
|
||||
* @deprecated
|
||||
*/
|
||||
export interface IRuleActionsAttributesSavedObjectAttributes
|
||||
extends IRuleActionsAttributes,
|
||||
SavedObjectAttributes {}
|
||||
|
||||
export interface RuleActionsResponse {
|
||||
[key: string]: {
|
||||
actions: IRuleActionsAttributes | null | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRuleActionsSavedObject {
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: Array<SavedObject<IRuleActionsAttributes & SavedObjectAttributes>>;
|
||||
references: unknown[];
|
||||
updated_at: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface IRuleActionsFindType {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
saved_objects: IRuleActionsSavedObject[];
|
||||
}
|
||||
|
||||
export const isRuleActionsSavedObjectType = (
|
||||
obj: unknown
|
||||
): obj is SavedObject<IRuleActionsAttributesSavedObjectAttributes> => {
|
||||
return get('attributes', obj) != null;
|
||||
};
|
||||
|
||||
export const isRuleActionsFindType = (
|
||||
obj: unknown
|
||||
): obj is SavedObjectsFindResponse<IRuleActionsAttributesSavedObjectAttributes> => {
|
||||
return get('saved_objects', obj) != null;
|
||||
};
|
||||
|
||||
export const isRuleActionsFindTypes = (
|
||||
obj: unknown[] | undefined
|
||||
): obj is Array<SavedObjectsFindResponse<IRuleActionsAttributesSavedObjectAttributes>> => {
|
||||
return obj ? obj.every((ruleStatus) => isRuleActionsFindType(ruleStatus)) : false;
|
||||
};
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertServices } from '../../../../../alerting/server';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { getRuleActionsSavedObject } from './get_rule_actions_saved_object';
|
||||
import { createRuleActionsSavedObject } from './create_rule_actions_saved_object';
|
||||
import { updateRuleActionsSavedObject } from './update_rule_actions_saved_object';
|
||||
import { RuleActions } from './types';
|
||||
|
||||
interface UpdateOrCreateRuleActionsSavedObject {
|
||||
ruleAlertId: string;
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
actions: RuleAlertAction[] | undefined;
|
||||
throttle: string | null | undefined;
|
||||
}
|
||||
|
||||
export const updateOrCreateRuleActionsSavedObject = async ({
|
||||
savedObjectsClient,
|
||||
ruleAlertId,
|
||||
actions,
|
||||
throttle,
|
||||
}: UpdateOrCreateRuleActionsSavedObject): Promise<RuleActions> => {
|
||||
const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient });
|
||||
|
||||
if (ruleActions != null) {
|
||||
return updateRuleActionsSavedObject({
|
||||
ruleAlertId,
|
||||
savedObjectsClient,
|
||||
actions,
|
||||
throttle,
|
||||
ruleActions,
|
||||
});
|
||||
} else {
|
||||
return createRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle });
|
||||
}
|
||||
};
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* 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 { AlertServices } from '../../../../../alerting/server';
|
||||
import { ruleActionsSavedObjectType } from './saved_object_mappings';
|
||||
import { RulesActionsSavedObject } from './get_rule_actions_saved_object';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { getThrottleOptions } from './utils';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes } from './types';
|
||||
|
||||
interface DeleteRuleActionsSavedObject {
|
||||
ruleAlertId: string;
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
actions: RuleAlertAction[] | undefined;
|
||||
throttle: string | null | undefined;
|
||||
ruleActions: RulesActionsSavedObject;
|
||||
}
|
||||
|
||||
export const updateRuleActionsSavedObject = async ({
|
||||
ruleAlertId,
|
||||
savedObjectsClient,
|
||||
actions,
|
||||
throttle,
|
||||
ruleActions,
|
||||
}: DeleteRuleActionsSavedObject): Promise<RulesActionsSavedObject> => {
|
||||
const throttleOptions = throttle
|
||||
? getThrottleOptions(throttle)
|
||||
: {
|
||||
ruleThrottle: ruleActions.ruleThrottle,
|
||||
alertThrottle: ruleActions.alertThrottle,
|
||||
};
|
||||
|
||||
const options = {
|
||||
actions: actions ?? ruleActions.actions,
|
||||
...throttleOptions,
|
||||
};
|
||||
|
||||
await savedObjectsClient.update<IRuleActionsAttributesSavedObjectAttributes>(
|
||||
ruleActionsSavedObjectType,
|
||||
ruleActions.id,
|
||||
{
|
||||
ruleAlertId,
|
||||
...options,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: ruleActions.id,
|
||||
...options,
|
||||
};
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* 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 { SavedObjectsUpdateResponse } from 'kibana/server';
|
||||
import { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { IRuleActionsAttributesSavedObjectAttributes } from './types';
|
||||
|
||||
export const getThrottleOptions = (
|
||||
throttle: string | undefined | null = 'no_actions'
|
||||
): {
|
||||
ruleThrottle: string;
|
||||
alertThrottle: string | null;
|
||||
} => ({
|
||||
ruleThrottle: throttle ?? 'no_actions',
|
||||
alertThrottle: ['no_actions', 'rule'].includes(throttle ?? 'no_actions') ? null : throttle,
|
||||
});
|
||||
|
||||
export const getRuleActionsFromSavedObject = (
|
||||
savedObject: SavedObjectsUpdateResponse<IRuleActionsAttributesSavedObjectAttributes>
|
||||
): {
|
||||
id: string;
|
||||
actions: RuleAlertAction[];
|
||||
alertThrottle: string | null;
|
||||
ruleThrottle: string;
|
||||
} => ({
|
||||
id: savedObject.id,
|
||||
actions: savedObject.attributes.actions || [],
|
||||
alertThrottle: savedObject.attributes.alertThrottle || null,
|
||||
ruleThrottle: savedObject.attributes.ruleThrottle || 'no_actions',
|
||||
});
|
|
@ -33,6 +33,7 @@ import { createResultObject } from './utils';
|
|||
import { bulkCreateFactory, wrapHitsFactory } from './factories';
|
||||
import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions';
|
||||
|
||||
/* eslint-disable complexity */
|
||||
export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
|
||||
|
@ -50,6 +51,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
|
|||
alertId,
|
||||
params,
|
||||
previousStartedAt,
|
||||
startedAt,
|
||||
services,
|
||||
spaceId,
|
||||
state,
|
||||
|
@ -277,7 +279,20 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
|
|||
|
||||
logger.info(buildRuleMessage(`Found ${createdSignalsCount} signals for notification.`));
|
||||
|
||||
if (createdSignalsCount) {
|
||||
if (ruleSO.attributes.throttle != null) {
|
||||
await scheduleThrottledNotificationActions({
|
||||
alertInstance: services.alertInstanceFactory(alertId),
|
||||
throttle: ruleSO.attributes.throttle,
|
||||
startedAt,
|
||||
id: ruleSO.id,
|
||||
kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined)
|
||||
?.kibana_siem_app_url,
|
||||
outputIndex: ruleDataClient.indexName,
|
||||
ruleId,
|
||||
esClient: services.scopedClusterClient.asCurrentUser,
|
||||
notificationRuleParams,
|
||||
});
|
||||
} else if (createdSignalsCount) {
|
||||
const alertInstance = services.alertInstanceFactory(alertId);
|
||||
scheduleNotificationActions({
|
||||
alertInstance,
|
||||
|
|
|
@ -51,6 +51,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
threatIndicatorPath: undefined,
|
||||
threshold: undefined,
|
||||
timestampOverride: undefined,
|
||||
throttle: null,
|
||||
to: 'now',
|
||||
type: 'query',
|
||||
references: ['http://www.example.com'],
|
||||
|
@ -103,6 +104,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
itemsPerSearch: undefined,
|
||||
threshold: undefined,
|
||||
timestampOverride: undefined,
|
||||
throttle: null,
|
||||
to: 'now',
|
||||
type: 'machine_learning',
|
||||
references: ['http://www.example.com'],
|
||||
|
|
|
@ -11,10 +11,15 @@ import {
|
|||
} from '../../../../common/detection_engine/utils';
|
||||
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
|
||||
import { SanitizedAlert } from '../../../../../alerting/common';
|
||||
import { SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants';
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
SERVER_APP_ID,
|
||||
SIGNALS_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { CreateRulesOptions } from './types';
|
||||
import { addTags } from './add_tags';
|
||||
import { PartialFilter, RuleTypeParams } from '../types';
|
||||
import { transformToAlertThrottle, transformToNotifyWhen } from './utils';
|
||||
|
||||
export const createRules = async ({
|
||||
rulesClient,
|
||||
|
@ -59,6 +64,7 @@ export const createRules = async ({
|
|||
threatMapping,
|
||||
threshold,
|
||||
timestampOverride,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
|
@ -67,7 +73,7 @@ export const createRules = async ({
|
|||
exceptionsList,
|
||||
actions,
|
||||
}: CreateRulesOptions): Promise<SanitizedAlert<RuleTypeParams>> => {
|
||||
return rulesClient.create<RuleTypeParams>({
|
||||
const rule = await rulesClient.create<RuleTypeParams>({
|
||||
data: {
|
||||
name,
|
||||
tags: addTags(tags, ruleId, immutable),
|
||||
|
@ -126,8 +132,15 @@ export const createRules = async ({
|
|||
schedule: { interval },
|
||||
enabled,
|
||||
actions: actions.map(transformRuleToAlertAction),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
throttle: transformToAlertThrottle(throttle),
|
||||
notifyWhen: transformToNotifyWhen(throttle),
|
||||
},
|
||||
});
|
||||
|
||||
// Mute the rule if it is first created with the explicit no actions
|
||||
if (throttle === NOTIFICATION_THROTTLE_NO_ACTIONS) {
|
||||
await rulesClient.muteAll({ id: rule.id });
|
||||
}
|
||||
|
||||
return rule;
|
||||
};
|
||||
|
|
|
@ -5,30 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
|
||||
import { rulesClientMock } from '../../../../../alerting/server/mocks';
|
||||
import { deleteRules } from './delete_rules';
|
||||
import { deleteNotifications } from '../notifications/delete_notifications';
|
||||
import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object';
|
||||
import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
|
||||
import { IRuleStatusSOAttributes } from './types';
|
||||
import { DeleteRuleOptions, IRuleStatusSOAttributes } from './types';
|
||||
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
|
||||
|
||||
jest.mock('../notifications/delete_notifications');
|
||||
jest.mock('../rule_actions/delete_rule_actions_saved_object');
|
||||
|
||||
describe('deleteRules', () => {
|
||||
let rulesClient: ReturnType<typeof rulesClientMock.create>;
|
||||
let ruleStatusClient: ReturnType<typeof ruleExecutionLogClientMock.create>;
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
|
||||
beforeEach(() => {
|
||||
rulesClient = rulesClientMock.create();
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
ruleStatusClient = ruleExecutionLogClientMock.create();
|
||||
});
|
||||
|
||||
it('should delete the rule along with its notifications, actions, and statuses', async () => {
|
||||
it('should delete the rule along with its actions, and statuses', async () => {
|
||||
const ruleStatus: SavedObjectsFindResult<IRuleStatusSOAttributes> = {
|
||||
id: 'statusId',
|
||||
type: '',
|
||||
|
@ -49,9 +41,8 @@ describe('deleteRules', () => {
|
|||
score: 0,
|
||||
};
|
||||
|
||||
const rule = {
|
||||
const rule: DeleteRuleOptions = {
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
ruleStatusClient,
|
||||
id: 'ruleId',
|
||||
ruleStatuses: [ruleStatus],
|
||||
|
@ -60,14 +51,6 @@ describe('deleteRules', () => {
|
|||
await deleteRules(rule);
|
||||
|
||||
expect(rulesClient.delete).toHaveBeenCalledWith({ id: rule.id });
|
||||
expect(deleteNotifications).toHaveBeenCalledWith({
|
||||
ruleAlertId: rule.id,
|
||||
rulesClient: expect.any(Object),
|
||||
});
|
||||
expect(deleteRuleActionsSavedObject).toHaveBeenCalledWith({
|
||||
ruleAlertId: rule.id,
|
||||
savedObjectsClient: expect.any(Object),
|
||||
});
|
||||
expect(ruleStatusClient.delete).toHaveBeenCalledWith(ruleStatus.id);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,19 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { deleteNotifications } from '../notifications/delete_notifications';
|
||||
import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object';
|
||||
import { DeleteRuleOptions } from './types';
|
||||
|
||||
export const deleteRules = async ({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
ruleStatusClient,
|
||||
ruleStatuses,
|
||||
id,
|
||||
}: DeleteRuleOptions) => {
|
||||
await rulesClient.delete({ id });
|
||||
await deleteNotifications({ rulesClient, ruleAlertId: id });
|
||||
await deleteRuleActionsSavedObject({ ruleAlertId: id, savedObjectsClient });
|
||||
ruleStatuses.forEach(async (obj) => ruleStatusClient.delete(obj.id));
|
||||
};
|
||||
|
|
|
@ -19,7 +19,9 @@ export const getExportAll = async (
|
|||
}> => {
|
||||
const ruleAlertTypes = await getNonPackagedRules({ rulesClient });
|
||||
const rules = transformAlertsToRules(ruleAlertTypes);
|
||||
const rulesNdjson = transformDataToNdjson(rules);
|
||||
// We do not support importing/exporting actions. When we do, delete this line of code
|
||||
const rulesWithoutActions = rules.map((rule) => ({ ...rule, actions: [] }));
|
||||
const rulesNdjson = transformDataToNdjson(rulesWithoutActions);
|
||||
const exportDetails = getExportDetailsNdjson(rules);
|
||||
return { rulesNdjson, exportDetails };
|
||||
};
|
||||
|
|
|
@ -40,8 +40,10 @@ export const getExportByObjectIds = async (
|
|||
exportDetails: string;
|
||||
}> => {
|
||||
const rulesAndErrors = await getRulesFromObjects(rulesClient, objects);
|
||||
const rulesNdjson = transformDataToNdjson(rulesAndErrors.rules);
|
||||
const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules);
|
||||
// We do not support importing/exporting actions. When we do, delete this line of code
|
||||
const rulesWithoutActions = rulesAndErrors.rules.map((rule) => ({ ...rule, actions: [] }));
|
||||
const rulesNdjson = transformDataToNdjson(rulesWithoutActions);
|
||||
const exportDetails = getExportDetailsNdjson(rulesWithoutActions, rulesAndErrors.missingRules);
|
||||
return { rulesNdjson, exportDetails };
|
||||
};
|
||||
|
||||
|
|
|
@ -113,6 +113,7 @@ export const installPrepackagedRules = (
|
|||
threatIndex,
|
||||
threatIndicatorPath,
|
||||
threshold,
|
||||
throttle: null, // At this time there is no pre-packaged actions
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
|
|
|
@ -50,6 +50,7 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({
|
|||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
throttle: null,
|
||||
concurrentSearches: undefined,
|
||||
itemsPerSearch: undefined,
|
||||
timestampOverride: undefined,
|
||||
|
@ -102,6 +103,7 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({
|
|||
threatQuery: undefined,
|
||||
threatMapping: undefined,
|
||||
threatLanguage: undefined,
|
||||
throttle: null,
|
||||
concurrentSearches: undefined,
|
||||
itemsPerSearch: undefined,
|
||||
timestampOverride: undefined,
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
import { patchRules } from './patch_rules';
|
||||
import { getPatchRulesOptionsMock, getPatchMlRulesOptionsMock } from './patch_rules.mock';
|
||||
import { PatchRulesOptions } from './types';
|
||||
import { RulesClientMock } from '../../../../../alerting/server/rules_client.mock';
|
||||
import { getAlertMock } from '../routes/__mocks__/request_responses';
|
||||
import { getQueryRuleParams } from '../schemas/rule_schemas.mock';
|
||||
|
||||
describe('patchRules', () => {
|
||||
it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => {
|
||||
|
@ -16,6 +19,9 @@ describe('patchRules', () => {
|
|||
...rulesOptionsMock,
|
||||
enabled: false,
|
||||
};
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
await patchRules(ruleOptions);
|
||||
expect(ruleOptions.rulesClient.disable).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -33,6 +39,9 @@ describe('patchRules', () => {
|
|||
if (ruleOptions.rule != null) {
|
||||
ruleOptions.rule.enabled = false;
|
||||
}
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
await patchRules(ruleOptions);
|
||||
expect(ruleOptions.rulesClient.enable).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -50,6 +59,9 @@ describe('patchRules', () => {
|
|||
if (ruleOptions.rule != null) {
|
||||
ruleOptions.rule.enabled = false;
|
||||
}
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
await patchRules(ruleOptions);
|
||||
expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -73,6 +85,9 @@ describe('patchRules', () => {
|
|||
if (ruleOptions.rule != null) {
|
||||
ruleOptions.rule.enabled = false;
|
||||
}
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
await patchRules(ruleOptions);
|
||||
expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -102,6 +117,9 @@ describe('patchRules', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
await patchRules(ruleOptions);
|
||||
expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -136,7 +154,9 @@ describe('patchRules', () => {
|
|||
},
|
||||
];
|
||||
}
|
||||
|
||||
((ruleOptions.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
await patchRules(ruleOptions);
|
||||
expect(ruleOptions.rulesClient.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -17,7 +17,15 @@ import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas';
|
|||
import { addTags } from './add_tags';
|
||||
import { enableRule } from './enable_rule';
|
||||
import { PatchRulesOptions } from './types';
|
||||
import { calculateInterval, calculateName, calculateVersion, removeUndefined } from './utils';
|
||||
import {
|
||||
calculateInterval,
|
||||
calculateName,
|
||||
calculateVersion,
|
||||
maybeMute,
|
||||
removeUndefined,
|
||||
transformToAlertThrottle,
|
||||
transformToNotifyWhen,
|
||||
} from './utils';
|
||||
|
||||
class PatchError extends Error {
|
||||
public readonly statusCode: number;
|
||||
|
@ -68,6 +76,7 @@ export const patchRules = async ({
|
|||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
timestampOverride,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
|
@ -179,8 +188,8 @@ export const patchRules = async ({
|
|||
|
||||
const newRule = {
|
||||
tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable),
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
throttle: throttle !== undefined ? transformToAlertThrottle(throttle) : rule.throttle,
|
||||
notifyWhen: throttle !== undefined ? transformToNotifyWhen(throttle) : rule.notifyWhen,
|
||||
name: calculateName({ updatedName: name, originalName: rule.name }),
|
||||
schedule: {
|
||||
interval: calculateInterval(interval, rule.schedule.interval),
|
||||
|
@ -188,6 +197,7 @@ export const patchRules = async ({
|
|||
actions: actions?.map(transformRuleToAlertAction) ?? rule.actions,
|
||||
params: removeUndefined(nextParams),
|
||||
};
|
||||
|
||||
const [validated, errors] = validate(newRule, internalRuleUpdate);
|
||||
if (errors != null || validated === null) {
|
||||
throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400);
|
||||
|
@ -198,6 +208,10 @@ export const patchRules = async ({
|
|||
data: validated,
|
||||
});
|
||||
|
||||
if (throttle !== undefined) {
|
||||
await maybeMute({ rulesClient, muteAll: rule.muteAll, throttle, id: update.id });
|
||||
}
|
||||
|
||||
if (rule.enabled && enabled === false) {
|
||||
await rulesClient.disable({ id: rule.id });
|
||||
} else if (!rule.enabled && enabled === true) {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
SavedObject,
|
||||
SavedObjectAttributes,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResult,
|
||||
} from 'kibana/server';
|
||||
import type {
|
||||
|
@ -42,6 +41,8 @@ import type {
|
|||
Severity,
|
||||
MaxSignalsOrUndefined,
|
||||
MaxSignals,
|
||||
ThrottleOrUndefinedOrNull,
|
||||
ThrottleOrNull,
|
||||
} from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { VersionOrUndefined, Version } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
|
@ -256,6 +257,7 @@ export interface CreateRulesOptions {
|
|||
concurrentSearches: ConcurrentSearchesOrUndefined;
|
||||
itemsPerSearch: ItemsPerSearchOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
throttle: ThrottleOrNull;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: To;
|
||||
type: Type;
|
||||
|
@ -315,6 +317,7 @@ export interface PatchRulesOptions {
|
|||
threatQuery: ThreatQueryOrUndefined;
|
||||
threatMapping: ThreatMappingOrUndefined;
|
||||
threatLanguage: ThreatLanguageOrUndefined;
|
||||
throttle: ThrottleOrUndefinedOrNull;
|
||||
timestampOverride: TimestampOverrideOrUndefined;
|
||||
to: ToOrUndefined;
|
||||
type: TypeOrUndefined;
|
||||
|
@ -334,7 +337,6 @@ export interface ReadRuleOptions {
|
|||
|
||||
export interface DeleteRuleOptions {
|
||||
rulesClient: RulesClient;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
ruleStatusClient: IRuleExecutionLogClient;
|
||||
ruleStatuses: Array<SavedObjectsFindResult<IRuleStatusSOAttributes>>;
|
||||
id: Id;
|
||||
|
|
|
@ -125,6 +125,7 @@ export const createPromises = (
|
|||
references,
|
||||
version,
|
||||
note,
|
||||
throttle,
|
||||
anomaly_threshold: anomalyThreshold,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
|
@ -188,6 +189,7 @@ export const createPromises = (
|
|||
timelineTitle,
|
||||
machineLearningJobId,
|
||||
exceptionsList,
|
||||
throttle,
|
||||
actions: undefined,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,9 @@ describe('updateRules', () => {
|
|||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).get.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
|
||||
await updateRules(rulesOptionsMock);
|
||||
|
||||
|
@ -36,6 +39,9 @@ describe('updateRules', () => {
|
|||
...getAlertMock(getQueryRuleParams()),
|
||||
enabled: false,
|
||||
});
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getQueryRuleParams())
|
||||
);
|
||||
|
||||
await updateRules(rulesOptionsMock);
|
||||
|
||||
|
@ -50,6 +56,10 @@ describe('updateRules', () => {
|
|||
const rulesOptionsMock = getUpdateMlRulesOptionsMock();
|
||||
rulesOptionsMock.ruleUpdate.enabled = true;
|
||||
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).update.mockResolvedValue(
|
||||
getAlertMock(getMlRuleParams())
|
||||
);
|
||||
|
||||
((rulesOptionsMock.rulesClient as unknown) as RulesClientMock).get.mockResolvedValue(
|
||||
getAlertMock(getMlRuleParams())
|
||||
);
|
||||
|
|
|
@ -16,6 +16,7 @@ import { addTags } from './add_tags';
|
|||
import { typeSpecificSnakeToCamel } from '../schemas/rule_converters';
|
||||
import { InternalRuleUpdate, RuleParams } from '../schemas/rule_schemas';
|
||||
import { enableRule } from './enable_rule';
|
||||
import { maybeMute, transformToAlertThrottle, transformToNotifyWhen } from './utils';
|
||||
|
||||
export const updateRules = async ({
|
||||
spaceId,
|
||||
|
@ -73,12 +74,9 @@ export const updateRules = async ({
|
|||
...typeSpecificParams,
|
||||
},
|
||||
schedule: { interval: ruleUpdate.interval ?? '5m' },
|
||||
actions:
|
||||
ruleUpdate.throttle === 'rule'
|
||||
? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction)
|
||||
: [],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: ruleUpdate.actions != null ? ruleUpdate.actions.map(transformRuleToAlertAction) : [],
|
||||
throttle: transformToAlertThrottle(ruleUpdate.throttle),
|
||||
notifyWhen: transformToNotifyWhen(ruleUpdate.throttle),
|
||||
};
|
||||
|
||||
const update = await rulesClient.update({
|
||||
|
@ -86,6 +84,13 @@ export const updateRules = async ({
|
|||
data: newInternalRule,
|
||||
});
|
||||
|
||||
await maybeMute({
|
||||
rulesClient,
|
||||
muteAll: existingRule.muteAll,
|
||||
throttle: ruleUpdate.throttle,
|
||||
id: update.id,
|
||||
});
|
||||
|
||||
if (existingRule.enabled && enabled === false) {
|
||||
await rulesClient.disable({ id: existingRule.id });
|
||||
} else if (!existingRule.enabled && enabled === true) {
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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 { RuleAlertAction } from '../../../../common/detection_engine/types';
|
||||
import { RulesClient, AlertServices } from '../../../../../alerting/server';
|
||||
import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object';
|
||||
import { updateNotifications } from '../notifications/update_notifications';
|
||||
import { RuleActions } from '../rule_actions/types';
|
||||
|
||||
interface UpdateRulesNotifications {
|
||||
rulesClient: RulesClient;
|
||||
savedObjectsClient: AlertServices['savedObjectsClient'];
|
||||
ruleAlertId: string;
|
||||
actions: RuleAlertAction[] | undefined;
|
||||
throttle: string | null | undefined;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const updateRulesNotifications = async ({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
ruleAlertId,
|
||||
actions,
|
||||
enabled,
|
||||
name,
|
||||
throttle,
|
||||
}: UpdateRulesNotifications): Promise<RuleActions> => {
|
||||
const ruleActions = await updateOrCreateRuleActionsSavedObject({
|
||||
savedObjectsClient,
|
||||
ruleAlertId,
|
||||
actions,
|
||||
throttle,
|
||||
});
|
||||
|
||||
await updateNotifications({
|
||||
rulesClient,
|
||||
ruleAlertId,
|
||||
enabled,
|
||||
name,
|
||||
actions: ruleActions.actions,
|
||||
interval: ruleActions.alertThrottle,
|
||||
});
|
||||
|
||||
return ruleActions;
|
||||
};
|
|
@ -5,7 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { calculateInterval, calculateVersion, calculateName } from './utils';
|
||||
import {
|
||||
calculateInterval,
|
||||
calculateVersion,
|
||||
calculateName,
|
||||
transformToNotifyWhen,
|
||||
transformToAlertThrottle,
|
||||
transformFromAlertThrottle,
|
||||
} from './utils';
|
||||
import { SanitizedAlert } from '../../../../../alerting/common';
|
||||
import { RuleParams } from '../schemas/rule_schemas';
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('#calculateInterval', () => {
|
||||
|
@ -198,4 +211,137 @@ describe('utils', () => {
|
|||
expect(name).toEqual('untitled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#transformToNotifyWhen', () => {
|
||||
test('"null" throttle returns "null" notify', () => {
|
||||
expect(transformToNotifyWhen(null)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"undefined" throttle returns "null" notify', () => {
|
||||
expect(transformToNotifyWhen(undefined)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"NOTIFICATION_THROTTLE_NO_ACTIONS" throttle returns "null" notify', () => {
|
||||
expect(transformToNotifyWhen(NOTIFICATION_THROTTLE_NO_ACTIONS)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"NOTIFICATION_THROTTLE_RULE" throttle returns "onActiveAlert" notify', () => {
|
||||
expect(transformToNotifyWhen(NOTIFICATION_THROTTLE_RULE)).toEqual('onActiveAlert');
|
||||
});
|
||||
|
||||
test('"1h" throttle returns "onThrottleInterval" notify', () => {
|
||||
expect(transformToNotifyWhen('1d')).toEqual('onThrottleInterval');
|
||||
});
|
||||
|
||||
test('"1d" throttle returns "onThrottleInterval" notify', () => {
|
||||
expect(transformToNotifyWhen('1d')).toEqual('onThrottleInterval');
|
||||
});
|
||||
|
||||
test('"7d" throttle returns "onThrottleInterval" notify', () => {
|
||||
expect(transformToNotifyWhen('7d')).toEqual('onThrottleInterval');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#transformToAlertThrottle', () => {
|
||||
test('"null" throttle returns "null" alert throttle', () => {
|
||||
expect(transformToAlertThrottle(null)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"undefined" throttle returns "null" alert throttle', () => {
|
||||
expect(transformToAlertThrottle(undefined)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"NOTIFICATION_THROTTLE_NO_ACTIONS" throttle returns "null" alert throttle', () => {
|
||||
expect(transformToAlertThrottle(NOTIFICATION_THROTTLE_NO_ACTIONS)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"NOTIFICATION_THROTTLE_RULE" throttle returns "null" alert throttle', () => {
|
||||
expect(transformToAlertThrottle(NOTIFICATION_THROTTLE_RULE)).toEqual(null);
|
||||
});
|
||||
|
||||
test('"1h" throttle returns "1h" alert throttle', () => {
|
||||
expect(transformToAlertThrottle('1h')).toEqual('1h');
|
||||
});
|
||||
|
||||
test('"1d" throttle returns "1d" alert throttle', () => {
|
||||
expect(transformToAlertThrottle('1d')).toEqual('1d');
|
||||
});
|
||||
|
||||
test('"7d" throttle returns "7d" alert throttle', () => {
|
||||
expect(transformToAlertThrottle('7d')).toEqual('7d');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#transformFromAlertThrottle', () => {
|
||||
test('muteAll returns "NOTIFICATION_THROTTLE_NO_ACTIONS" even with notifyWhen set and actions has an array element', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle({
|
||||
muteAll: true,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as SanitizedAlert<RuleParams>)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('returns "NOTIFICATION_THROTTLE_NO_ACTIONS" if actions is an empty array and we do not have a throttle', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(({
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [],
|
||||
} as unknown) as SanitizedAlert<RuleParams>)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('returns "NOTIFICATION_THROTTLE_NO_ACTIONS" if actions is an empty array and we have a throttle', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle(({
|
||||
muteAll: false,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
actions: [],
|
||||
throttle: '1d',
|
||||
} as unknown) as SanitizedAlert<RuleParams>)
|
||||
).toEqual(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
test('it returns "NOTIFICATION_THROTTLE_RULE" if "notifyWhen" is set, muteAll is false and we have an actions array', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle({
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActiveAlert',
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as SanitizedAlert<RuleParams>)
|
||||
).toEqual(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
test('it returns "NOTIFICATION_THROTTLE_RULE" if "notifyWhen" and "throttle" are not set, but we have an actions array', () => {
|
||||
expect(
|
||||
transformFromAlertThrottle({
|
||||
muteAll: false,
|
||||
actions: [
|
||||
{
|
||||
group: 'group',
|
||||
id: 'id-123',
|
||||
actionTypeId: 'id-456',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
} as SanitizedAlert<RuleParams>)
|
||||
).toEqual(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ import type {
|
|||
} from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { ListArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types';
|
||||
import { AlertNotifyWhenType, SanitizedAlert } from '../../../../../alerting/common';
|
||||
import {
|
||||
DescriptionOrUndefined,
|
||||
AnomalyThresholdOrUndefined,
|
||||
|
@ -53,6 +54,12 @@ import {
|
|||
EventCategoryOverrideOrUndefined,
|
||||
} from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { PartialFilter } from '../types';
|
||||
import { RuleParams } from '../schemas/rule_schemas';
|
||||
import {
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
} from '../../../../common/constants';
|
||||
import { RulesClient } from '../../../../../alerting/server';
|
||||
|
||||
export const calculateInterval = (
|
||||
interval: string | undefined,
|
||||
|
@ -167,3 +174,87 @@ export const calculateName = ({
|
|||
return 'untitled';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a throttle from a "security_solution" rule this will transform it into an "alerting" notifyWhen
|
||||
* on their saved object.
|
||||
* @params throttle The throttle from a "security_solution" rule
|
||||
* @returns The correct "NotifyWhen" for a Kibana alerting.
|
||||
*/
|
||||
export const transformToNotifyWhen = (
|
||||
throttle: string | null | undefined
|
||||
): AlertNotifyWhenType | null => {
|
||||
if (throttle == null || throttle === NOTIFICATION_THROTTLE_NO_ACTIONS) {
|
||||
return null; // Although I return null, this does not change the value of the "notifyWhen" and it keeps the current value of "notifyWhen"
|
||||
} else if (throttle === NOTIFICATION_THROTTLE_RULE) {
|
||||
return 'onActiveAlert';
|
||||
} else {
|
||||
return 'onThrottleInterval';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a throttle from a "security_solution" rule this will transform it into an "alerting" "throttle"
|
||||
* on their saved object.
|
||||
* @params throttle The throttle from a "security_solution" rule
|
||||
* @returns The "alerting" throttle
|
||||
*/
|
||||
export const transformToAlertThrottle = (throttle: string | null | undefined): string | null => {
|
||||
if (
|
||||
throttle == null ||
|
||||
throttle === NOTIFICATION_THROTTLE_RULE ||
|
||||
throttle === NOTIFICATION_THROTTLE_NO_ACTIONS
|
||||
) {
|
||||
return null;
|
||||
} else {
|
||||
return throttle;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a throttle from an "alerting" Saved Object (SO) this will transform it into a "security_solution"
|
||||
* throttle type.
|
||||
* @params throttle The throttle from a "alerting" Saved Object (SO)
|
||||
* @returns The "security_solution" throttle
|
||||
*/
|
||||
export const transformFromAlertThrottle = (rule: SanitizedAlert<RuleParams>): string => {
|
||||
if (rule.muteAll || rule.actions.length === 0) {
|
||||
return NOTIFICATION_THROTTLE_NO_ACTIONS;
|
||||
} else if (
|
||||
rule.notifyWhen === 'onActiveAlert' ||
|
||||
(rule.throttle == null && rule.notifyWhen == null)
|
||||
) {
|
||||
return NOTIFICATION_THROTTLE_RULE;
|
||||
} else if (rule.throttle == null) {
|
||||
return NOTIFICATION_THROTTLE_NO_ACTIONS;
|
||||
} else {
|
||||
return rule.throttle;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutes, unmutes, or does nothing to the alert if no changed is detected
|
||||
* @param id The id of the alert to (un)mute
|
||||
* @param rulesClient the rules client
|
||||
* @param muteAll If the existing alert has all actions muted
|
||||
* @param throttle If the existing alert has a throttle set
|
||||
*/
|
||||
export const maybeMute = async ({
|
||||
id,
|
||||
rulesClient,
|
||||
muteAll,
|
||||
throttle,
|
||||
}: {
|
||||
id: SanitizedAlert['id'];
|
||||
rulesClient: RulesClient;
|
||||
muteAll: SanitizedAlert<RuleParams>['muteAll'];
|
||||
throttle: string | null | undefined;
|
||||
}): Promise<void> => {
|
||||
if (muteAll && throttle !== NOTIFICATION_THROTTLE_NO_ACTIONS) {
|
||||
await rulesClient.unmuteAll({ id });
|
||||
} else if (!muteAll && throttle === NOTIFICATION_THROTTLE_NO_ACTIONS) {
|
||||
await rulesClient.muteAll({ id });
|
||||
} else {
|
||||
// Do nothing, no-operation
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
FullResponseSchema,
|
||||
ResponseTypeSpecific,
|
||||
} from '../../../../common/detection_engine/schemas/request';
|
||||
import { RuleActions } from '../rule_actions/types';
|
||||
import { AppClient } from '../../../types';
|
||||
import { addTags } from '../rules/add_tags';
|
||||
import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants';
|
||||
|
@ -32,6 +31,11 @@ import { SanitizedAlert } from '../../../../../alerting/common';
|
|||
import { IRuleStatusSOAttributes } from '../rules/types';
|
||||
import { transformTags } from '../routes/rules/utils';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import {
|
||||
transformFromAlertThrottle,
|
||||
transformToAlertThrottle,
|
||||
transformToNotifyWhen,
|
||||
} from '../rules/utils';
|
||||
|
||||
// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema
|
||||
// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for
|
||||
|
@ -156,9 +160,9 @@ export const convertCreateAPIToInternalSchema = (
|
|||
},
|
||||
schedule: { interval: input.interval ?? '5m' },
|
||||
enabled: input.enabled ?? true,
|
||||
actions: input.throttle === 'rule' ? (input.actions ?? []).map(transformRuleToAlertAction) : [],
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: input.actions?.map(transformRuleToAlertAction) ?? [],
|
||||
throttle: transformToAlertThrottle(input.throttle),
|
||||
notifyWhen: transformToNotifyWhen(input.throttle),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -271,7 +275,6 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => {
|
|||
|
||||
export const internalRuleToAPIResponse = (
|
||||
rule: SanitizedAlert<RuleParams>,
|
||||
ruleActions?: RuleActions | null,
|
||||
ruleStatus?: IRuleStatusSOAttributes
|
||||
): FullResponseSchema => {
|
||||
const mergedStatus = ruleStatus ? mergeAlertWithSidecarStatus(rule, ruleStatus) : undefined;
|
||||
|
@ -291,8 +294,14 @@ export const internalRuleToAPIResponse = (
|
|||
// Type specific security solution rule params
|
||||
...typeSpecificCamelToSnake(rule.params),
|
||||
// Actions
|
||||
throttle: ruleActions?.ruleThrottle || 'no_actions',
|
||||
actions: ruleActions?.actions ?? [],
|
||||
throttle: transformFromAlertThrottle(rule),
|
||||
actions:
|
||||
rule?.actions.map((action) => ({
|
||||
group: action.group,
|
||||
id: action.id,
|
||||
action_type_id: action.actionTypeId,
|
||||
params: action.params,
|
||||
})) ?? [],
|
||||
// Rule status
|
||||
status: mergedStatus?.status ?? undefined,
|
||||
status_date: mergedStatus?.statusDate ?? undefined,
|
||||
|
|
|
@ -189,6 +189,13 @@ export type TypeSpecificRuleParams = t.TypeOf<typeof typeSpecificRuleParams>;
|
|||
export const ruleParams = t.intersection([baseRuleParams, typeSpecificRuleParams]);
|
||||
export type RuleParams = t.TypeOf<typeof ruleParams>;
|
||||
|
||||
export const notifyWhen = t.union([
|
||||
t.literal('onActionGroupChange'),
|
||||
t.literal('onActiveAlert'),
|
||||
t.literal('onThrottleInterval'),
|
||||
t.null,
|
||||
]);
|
||||
|
||||
export const internalRuleCreate = t.type({
|
||||
name,
|
||||
tags,
|
||||
|
@ -201,7 +208,7 @@ export const internalRuleCreate = t.type({
|
|||
actions: actionsCamel,
|
||||
params: ruleParams,
|
||||
throttle: throttleOrNull,
|
||||
notifyWhen: t.null,
|
||||
notifyWhen,
|
||||
});
|
||||
export type InternalRuleCreate = t.TypeOf<typeof internalRuleCreate>;
|
||||
|
||||
|
@ -214,7 +221,7 @@ export const internalRuleUpdate = t.type({
|
|||
actions: actionsCamel,
|
||||
params: ruleParams,
|
||||
throttle: throttleOrNull,
|
||||
notifyWhen: t.null,
|
||||
notifyWhen,
|
||||
});
|
||||
export type InternalRuleUpdate = t.TypeOf<typeof internalRuleUpdate>;
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
} from './utils';
|
||||
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types';
|
||||
import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
|
||||
import { RuleAlertType } from '../rules/types';
|
||||
import { listMock } from '../../../../../lists/server/mocks';
|
||||
import { getListClientMock } from '../../../../../lists/server/services/lists/list_client.mock';
|
||||
|
@ -34,6 +33,7 @@ import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.moc
|
|||
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
|
||||
import { allowedExperimentalValues } from '../../../../common/experimental_features';
|
||||
import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks';
|
||||
import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
|
||||
import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
|
@ -329,12 +329,6 @@ describe('signal_rule_alert_type', () => {
|
|||
});
|
||||
|
||||
await alert.executor(payload);
|
||||
|
||||
expect(scheduleNotificationActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
signalsCount: 10,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve results_link when meta is an empty object to use "/app/security"', async () => {
|
||||
|
|
|
@ -72,6 +72,7 @@ import { injectReferences, extractReferences } from './saved_object_references';
|
|||
import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
|
||||
import { IRuleDataPluginService } from '../rule_execution_log/types';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions';
|
||||
|
||||
export const signalRulesAlertType = ({
|
||||
logger,
|
||||
|
@ -405,7 +406,20 @@ export const signalRulesAlertType = ({
|
|||
buildRuleMessage(`Found ${result.createdSignalsCount} signals for notification.`)
|
||||
);
|
||||
|
||||
if (result.createdSignalsCount) {
|
||||
if (savedObject.attributes.throttle != null) {
|
||||
await scheduleThrottledNotificationActions({
|
||||
alertInstance: services.alertInstanceFactory(alertId),
|
||||
throttle: savedObject.attributes.throttle,
|
||||
startedAt,
|
||||
id: savedObject.id,
|
||||
kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined)
|
||||
?.kibana_siem_app_url,
|
||||
outputIndex,
|
||||
ruleId,
|
||||
esClient: services.scopedClusterClient.asCurrentUser,
|
||||
notificationRuleParams,
|
||||
});
|
||||
} else if (result.createdSignalsCount) {
|
||||
const alertInstance = services.alertInstanceFactory(alertId);
|
||||
scheduleNotificationActions({
|
||||
alertInstance,
|
||||
|
|
|
@ -52,8 +52,6 @@ import { createQueryAlertType } from './lib/detection_engine/rule_types';
|
|||
import { initRoutes } from './routes';
|
||||
import { isAlertExecutor } from './lib/detection_engine/signals/types';
|
||||
import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type';
|
||||
import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type';
|
||||
import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types';
|
||||
import { ManifestTask } from './endpoint/lib/artifacts';
|
||||
import { initSavedObjects } from './saved_objects';
|
||||
import { AppClientFactory } from './client';
|
||||
|
@ -295,17 +293,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
experimentalFeatures,
|
||||
ruleDataService: plugins.ruleRegistry.ruleDataService,
|
||||
});
|
||||
const ruleNotificationType = rulesNotificationAlertType({
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
if (isAlertExecutor(signalRuleType)) {
|
||||
this.setupPlugins.alerting.registerType(signalRuleType);
|
||||
}
|
||||
|
||||
if (isNotificationAlertExecutor(ruleNotificationType)) {
|
||||
this.setupPlugins.alerting.registerType(ruleNotificationType);
|
||||
}
|
||||
}
|
||||
|
||||
const exceptionListsSetupEnabled = () => {
|
||||
|
|
|
@ -47,6 +47,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./delete_signals_migrations'));
|
||||
loadTestFile(require.resolve('./timestamps'));
|
||||
loadTestFile(require.resolve('./runtime'));
|
||||
loadTestFile(require.resolve('./throttle'));
|
||||
});
|
||||
|
||||
// That split here enable us on using a different ciGroup to run the tests
|
||||
|
|
|
@ -0,0 +1,357 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
|
||||
import {
|
||||
DETECTION_ENGINE_RULES_URL,
|
||||
NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
NOTIFICATION_THROTTLE_RULE,
|
||||
} from '../../../../plugins/security_solution/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getWebHookAction,
|
||||
getRuleWithWebHookAction,
|
||||
createRule,
|
||||
getSimpleRule,
|
||||
getRule,
|
||||
updateRule,
|
||||
} from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
/**
|
||||
*
|
||||
* These tests will ensure that the existing synchronization between the alerting API and its states of:
|
||||
* - "notifyWhen"
|
||||
* - "muteAll"
|
||||
* - "throttle"
|
||||
* Work within the security_solution's API and states of "throttle" which currently not a 1 to 1 relationship:
|
||||
*
|
||||
* Ref:
|
||||
* https://www.elastic.co/guide/en/kibana/master/create-and-manage-rules.html#controlling-rules
|
||||
* https://www.elastic.co/guide/en/kibana/current/mute-all-alerts-api.html
|
||||
* https://www.elastic.co/guide/en/security/current/rules-api-create.html
|
||||
*/
|
||||
describe('throttle', () => {
|
||||
describe('adding actions', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSignalsIndex(supertest);
|
||||
await deleteAllAlerts(supertest);
|
||||
});
|
||||
|
||||
describe('creating a rule', () => {
|
||||
it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const rule = await createRule(supertest, getRuleWithWebHookAction(hookAction.id));
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, the rule should have its kibana alerting "mute_all" set to "true" and notify_when set to "onActiveAlert"', async () => {
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getSimpleRule(),
|
||||
throttle: NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(true);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and with actions set, the rule should have its kibana alerting "mute_all" set to "true" and notify_when set to "onActiveAlert"', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getRuleWithWebHookAction(hookAction.id),
|
||||
throttle: NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(true);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => {
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getSimpleRule(),
|
||||
throttle: NOTIFICATION_THROTTLE_RULE,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
// NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty.
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, since we do not have any actions, we should get back a throttle of "NOTIFICATION_THROTTLE_NO_ACTIONS"', async () => {
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getSimpleRule(),
|
||||
throttle: NOTIFICATION_THROTTLE_RULE,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getRuleWithWebHookAction(hookAction.id),
|
||||
throttle: NOTIFICATION_THROTTLE_RULE,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => {
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getSimpleRule(),
|
||||
throttle: '1h',
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onThrottleInterval');
|
||||
});
|
||||
|
||||
it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getRuleWithWebHookAction(hookAction.id),
|
||||
throttle: '1h',
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onThrottleInterval');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reading a rule', () => {
|
||||
it('When creating a new action and attaching it to a rule, we should return "NOTIFICATION_THROTTLE_RULE" when doing a read', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const rule = await createRule(supertest, getRuleWithWebHookAction(hookAction.id));
|
||||
const readRule = await getRule(supertest, rule.rule_id);
|
||||
expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, we should return "NOTIFICATION_THROTTLE_NO_ACTIONS" when doing a read', async () => {
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getSimpleRule(),
|
||||
throttle: NOTIFICATION_THROTTLE_NO_ACTIONS,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const readRule = await getRule(supertest, rule.rule_id);
|
||||
expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
// NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty.
|
||||
it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, since we do not have any actions, we should get back a throttle of "NOTIFICATION_THROTTLE_NO_ACTIONS" when doing a read', async () => {
|
||||
const ruleWithThrottle: CreateRulesSchema = {
|
||||
...getSimpleRule(),
|
||||
throttle: NOTIFICATION_THROTTLE_RULE,
|
||||
};
|
||||
const rule = await createRule(supertest, ruleWithThrottle);
|
||||
const readRule = await getRule(supertest, rule.rule_id);
|
||||
expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
|
||||
it('When creating a new action and attaching it to a rule, if we change the alert to a "muteAll" through the kibana alerting API, we should get back "NOTIFICATION_THROTTLE_NO_ACTIONS" ', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const rule = await createRule(supertest, getRuleWithWebHookAction(hookAction.id));
|
||||
await supertest
|
||||
.post(`/api/alerting/rule/${rule.id}/_mute_all`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(204);
|
||||
const readRule = await getRule(supertest, rule.rule_id);
|
||||
expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updating a rule', () => {
|
||||
it('will not change "NOTIFICATION_THROTTLE_RULE" if we update some part of the rule', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithWebHookAction = getRuleWithWebHookAction(hookAction.id);
|
||||
await createRule(supertest, ruleWithWebHookAction);
|
||||
ruleWithWebHookAction.name = 'some other name';
|
||||
const updated = await updateRule(supertest, ruleWithWebHookAction);
|
||||
expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
it('will not change the "muteAll" or "notifyWhen" if we update some part of the rule', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithWebHookAction = getRuleWithWebHookAction(hookAction.id);
|
||||
await createRule(supertest, ruleWithWebHookAction);
|
||||
ruleWithWebHookAction.name = 'some other name';
|
||||
const updated = await updateRule(supertest, ruleWithWebHookAction);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${updated.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
// NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty.
|
||||
it('If we update a rule and remove just the actions array it will begin returning a throttle of "NOTIFICATION_THROTTLE_NO_ACTIONS"', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithWebHookAction = getRuleWithWebHookAction(hookAction.id);
|
||||
await createRule(supertest, ruleWithWebHookAction);
|
||||
ruleWithWebHookAction.actions = [];
|
||||
const updated = await updateRule(supertest, ruleWithWebHookAction);
|
||||
expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patching a rule', () => {
|
||||
it('will not change "NOTIFICATION_THROTTLE_RULE" if we patch some part of the rule', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithWebHookAction = getRuleWithWebHookAction(hookAction.id);
|
||||
const rule = await createRule(supertest, ruleWithWebHookAction);
|
||||
// patch a simple rule's name
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: rule.rule_id, name: 'some other name' })
|
||||
.expect(200);
|
||||
const readRule = await getRule(supertest, rule.rule_id);
|
||||
expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE);
|
||||
});
|
||||
|
||||
it('will not change the "muteAll" or "notifyWhen" if we patch part of the rule', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithWebHookAction = getRuleWithWebHookAction(hookAction.id);
|
||||
const rule = await createRule(supertest, ruleWithWebHookAction);
|
||||
// patch a simple rule's name
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: rule.rule_id, name: 'some other name' })
|
||||
.expect(200);
|
||||
const {
|
||||
body: { mute_all: muteAll, notify_when: notifyWhen },
|
||||
} = await supertest.get(`/api/alerting/rule/${rule.id}`);
|
||||
expect(muteAll).to.eql(false);
|
||||
expect(notifyWhen).to.eql('onActiveAlert');
|
||||
});
|
||||
|
||||
// NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty.
|
||||
it('If we patch a rule and remove just the actions array it will begin returning a throttle of "NOTIFICATION_THROTTLE_NO_ACTIONS"', async () => {
|
||||
// create a new action
|
||||
const { body: hookAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getWebHookAction())
|
||||
.expect(200);
|
||||
|
||||
const ruleWithWebHookAction = getRuleWithWebHookAction(hookAction.id);
|
||||
const rule = await createRule(supertest, ruleWithWebHookAction);
|
||||
// patch a simple rule's action
|
||||
await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ rule_id: rule.rule_id, actions: [] })
|
||||
.expect(200);
|
||||
const readRule = await getRule(supertest, rule.rule_id);
|
||||
expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Reference in a new issue