[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:
Frank Hassanabad 2021-08-26 11:39:57 -06:00 committed by GitHub
parent 2348ced4c0
commit ad01057f90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1111 additions and 1984 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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',

View file

@ -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({

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

@ -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 {

View file

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

View file

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

View file

@ -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 {

View file

@ -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();

View file

@ -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({

View file

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

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

@ -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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -113,6 +113,7 @@ export const installPrepackagedRules = (
threatIndex,
threatIndicatorPath,
threshold,
throttle: null, // At this time there is no pre-packaged actions
timestampOverride,
references,
note,

View file

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

View file

@ -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({

View file

@ -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) {

View file

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

View file

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

View file

@ -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())
);

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 () => {

View file

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

View file

@ -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 = () => {

View file

@ -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

View file

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