From 6ff91e8c94886dc5a97baedadd9f260c7154b448 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 15 Nov 2019 23:56:45 -0700 Subject: [PATCH] [SIEM][Detection Engine] REST API improvements and changes from UI/UX feedback (#50797) ## Summary Updated REST API from feedback from the UI/UX * Changes the `id` to be `rule_id` on PUT/POST and makes it optional for a POST (create). * On data return sets both `id` and `rule_id` is returned. If `rule_id` is not set, a uuid.v4() will b assigned to the rule_id and the value will be returned. * Transforms output of all endpoints to be 1-1 to the input. * Fixes delete to return the deleted rule * Changes the URL to be `/api/detection_engine/rules` * Changes the POST behavior to fail with a `409 (conflict)` if the rule already exists (For creates) * Changes the POST behavior where sending in a `rule_id` is now optional. If none are sent in it does not create a `rule_id` and instead returns `null` for the `rule_id` and the autogenerated one. * Changes the PUT behavior to fail with a `404 (not found)` if the rule does not already exist (For updates) * Deletes the actions code and just uses an empty array since we don't have actions yet * Makes all error conditions consistent and does not expose the underlying error codes. Only exception to the rule is if an error condition returns non `404` or something unexpected. In which case it will show that error upstream. Example post output: ```ts $ ./post_signal.sh { "created_by": "elastic", "description": "Detecting root and admin users", "enabled": true, "false_positives": [], "from": "now-6m", "id": "8277a0e8-474c-4507-9c11-5f197b5fe2d5", "index": [ "auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*" ], "interval": "5m", "rule_id": "rule-1", "language": "kuery", "max_signals": 100, "name": "Detect Root/Admin Users", "query": "user.name: root or user.name: admin", "references": [ "http://www.example.com", "https://ww.example.com" ], "severity": "high", "updated_by": "elastic", "tags": [], "to": "now", "type": "query" } ``` Example delete and get URL's now (see scripts for more details): ```ts ${KIBANA_URL}/api/detection_engine/rules?rule_id="rule-1" ${KIBANA_URL}/api/detection_engine/rules?id="04128c15-0d1b-4716-a4c5-46997ac7f3bd" ``` ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../legacy/plugins/siem/common/constants.ts | 6 + .../convert_saved_search_to_signals.js | 2 +- .../server/lib/detection_engine/README.md | 33 +- .../alerts/__mocks__/es_results.ts | 4 +- .../alerts/build_events_reindex.ts | 5 +- .../detection_engine/alerts/create_signals.ts | 176 ++------ .../detection_engine/alerts/delete_signals.ts | 46 +-- .../lib/detection_engine/alerts/get_filter.ts | 12 +- .../alerts/read_signals.test.ts | 238 +++++++++++ .../detection_engine/alerts/read_signals.ts | 41 +- .../alerts/signals_alert_type.ts | 34 +- .../lib/detection_engine/alerts/types.ts | 57 ++- .../detection_engine/alerts/update_signals.ts | 8 +- .../lib/detection_engine/alerts/utils.test.ts | 39 +- .../lib/detection_engine/alerts/utils.ts | 21 +- .../routes/__mocks__/request_responses.ts | 137 +++--- .../routes/create_signals_route.test.ts | 30 +- .../routes/create_signals_route.ts | 21 +- .../routes/delete_signals_route.test.ts | 35 +- .../routes/delete_signals_route.ts | 20 +- .../routes/find_signals_route.test.ts | 6 +- .../routes/find_signals_route.ts | 7 +- .../routes/read_signals_route.test.ts | 16 +- .../routes/read_signals_route.ts | 19 +- .../detection_engine/routes/schemas.test.ts | 389 ++++++++++++++---- .../lib/detection_engine/routes/schemas.ts | 11 +- .../routes/update_signals_route.test.ts | 82 ++-- .../routes/update_signals_route.ts | 25 +- .../lib/detection_engine/routes/utils.test.ts | 268 ++++++++++++ .../lib/detection_engine/routes/utils.ts | 73 ++++ ...elete_signal.sh => delete_signal_by_id.sh} | 4 +- .../scripts/delete_signal_by_rule_id.sh | 16 + .../detection_engine/scripts/find_signals.sh | 2 +- .../{get_signal.sh => get_signal_by_id.sh} | 4 +- .../scripts/get_signal_by_rule_id.sh | 15 + .../detection_engine/scripts/post_signal.sh | 2 +- .../scripts/post_x_signals.sh | 6 +- .../scripts/signals/root_or_admin_1.json | 2 +- .../scripts/signals/root_or_admin_10.json | 13 + .../scripts/signals/root_or_admin_2.json | 2 +- .../scripts/signals/root_or_admin_3.json | 2 +- .../scripts/signals/root_or_admin_4.json | 2 +- .../scripts/signals/root_or_admin_5.json | 2 +- .../scripts/signals/root_or_admin_6.json | 2 +- .../scripts/signals/root_or_admin_7.json | 2 +- .../scripts/signals/root_or_admin_8.json | 2 +- .../scripts/signals/root_or_admin_9.json | 2 +- .../signals/root_or_admin_filter_9998.json | 2 +- .../signals/root_or_admin_filter_9999.json | 2 +- .../signals/root_or_admin_saved_query_1.json | 2 +- .../signals/root_or_admin_update_1.json | 4 +- .../signals/root_or_admin_update_2.json | 2 +- .../scripts/signals/watch_longmont.json | 2 +- .../detection_engine/scripts/update_signal.sh | 27 +- .../lib/detection_engine/signals_mapping.json | 3 + .../legacy/plugins/siem/server/lib/types.ts | 3 +- 56 files changed, 1430 insertions(+), 558 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal.sh => delete_signal_by_id.sh} (77%) create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal.sh => get_signal_by_id.sh} (76%) create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 8755e432939546..e372fb02a8c44d 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -32,3 +32,9 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; * Id for the SIGNALS alerting type */ export const SIGNALS_ID = `${APP_ID}.signals`; + +/** + * Detection engine route + */ +export const DETECTION_ENGINE_URL = '/api/detection_engine'; +export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js index e6dcefd6fb4f8b..a1889a400a1835 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js @@ -118,7 +118,7 @@ async function main() { if (query != null && query.trim() !== '') { const outputMessage = { - id: fileToWrite, + rule_id: fileToWrite, description: description || title, immutable: IMMUTABLE, index: INDEX, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 2c635a17ef5834..5b2318e15a4695 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -5,7 +5,7 @@ See these two other pages for references: https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/actions -Since there is no UI yet and a lot of backend areas that are not created, you +Since there is no UI yet and a lot of backend areas that are not created, you should install the kbn-action and kbn-alert project from here: https://github.com/pmuellr/kbn-action @@ -18,6 +18,7 @@ brew install jq ``` Open up your .zshrc/.bashrc and add these lines with the variables filled in: + ``` export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} @@ -41,6 +42,7 @@ export USE_REINDEX_API=true ``` Add these lines to your `kibana.dev.yml` to turn on the feature toggles of alerting and actions: + ``` # Feature flag to turn on alerting xpack.alerting.enabled: true @@ -64,6 +66,7 @@ while commenting out the other require statement: ``` Restart Kibana and you should see alerting and actions starting up + ``` server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed from uninitialized to green - Ready server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready @@ -84,12 +87,12 @@ Open a terminal and go into the scripts folder `cd kibana/x-pack/legacy/plugins/ which will: -* Delete any existing actions you have -* Delete any existing alerts you have -* Delete any existing alert tasks you have -* Delete any existing signal mapping you might have had. -* Add the latest signal index and its mappings -* Posts a sample signal which checks for root or admin every 5 minutes +- Delete any existing actions you have +- Delete any existing alerts you have +- Delete any existing alert tasks you have +- Delete any existing signal mapping you might have had. +- Add the latest signal index and its mappings +- Posts a sample signal which checks for root or admin every 5 minutes Now you can run @@ -98,20 +101,13 @@ Now you can run ``` You should see the new alert instance created like so: + ```ts { "id": "908a6af1-ac63-4d52-a856-fc635a00db0f", "alertTypeId": "siem.signals", "interval": "5m", - "actions": [ - { - "group": "default", - "params": { - "message": "SIEM Alert Fired" - }, - "id": "7edd7e98-9286-4fdb-a5c5-16de776bc7c7" - } - ], + "actions": [ ], "alertTypeParams": {}, "enabled": true, "throttle": null, @@ -128,13 +124,13 @@ Every 5 minutes you should see this message in your terminal now: server log [22:17:33.945] [info][alerting] SIEM Alert Fired ``` -See the scripts folder and the tools for more command line fun. +See the scripts folder and the tools for more command line fun. Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals created which should update once every 5 minutes at this point. Also add the `.siem-signals-${your user id}` as a kibana index for Maps to be able to see the -signals +signals Optionally you can add these debug statements to your `kibana.dev.yml` to see more information when running the detection engine @@ -149,4 +145,3 @@ logging.events: ops: __no-ops__, } ``` - diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 895af32cc7af33..76df961535fcd3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -7,7 +7,7 @@ import { SignalSourceHit, SignalSearchResponse, SignalAlertParams } from '../types'; export const sampleSignalAlertParams = (maxSignals: number | undefined): SignalAlertParams => ({ - id: 'rule-1', + ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], immutable: false, @@ -148,3 +148,5 @@ export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { ], }, }; + +export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts index 4c5d981614cf10..0e8d95e4f7ac1b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts @@ -22,6 +22,7 @@ interface BuildEventsReIndexParams { timeDetected: string; ruleRevision: number; id: string; + ruleId: string | undefined | null; type: string; references: string[]; } @@ -39,6 +40,7 @@ export const buildEventsReIndex = ({ timeDetected, ruleRevision, id, + ruleId, type, references, }: BuildEventsReIndexParams) => { @@ -120,8 +122,9 @@ export const buildEventsReIndex = ({ ]; def signal = [ + "id": "${id}", "rule_revision": "${ruleRevision}", - "rule_id": "${id}", + "rule_id": "${ruleId}", "rule_type": "${type}", "parent": parent, "name": "${name}", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 73e3d96f5332e8..d8284c32036822 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -5,75 +5,11 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { updateSignal } from './update_signals'; import { SignalParams } from './types'; -// TODO: This updateIfIdExists should be temporary and we will remove it once we can POST id's directly to -// the alerting framework. -export const updateIfIdExists = async ({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - immutable, - query, - language, - savedId, - filters, - id, - index, - interval, - maxSignals, - name, - severity, - size, - tags, - to, - type, - references, -}: SignalParams) => { - try { - const signal = await updateSignal({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - immutable, - query, - language, - savedId, - filters, - id, - index, - interval, - maxSignals, - name, - severity, - size, - tags, - to, - type, - references, - }); - return signal; - } catch (err) { - if (err.output.statusCode === 404) { - return null; - } else { - return err; - } - } -}; - export const createSignals = async ({ alertsClient, - actionsClient, + actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... description, enabled, falsePositives, @@ -83,7 +19,7 @@ export const createSignals = async ({ language, savedId, filters, - id, + ruleId, immutable, index, interval, @@ -96,85 +32,33 @@ export const createSignals = async ({ type, references, }: SignalParams) => { - // TODO: Once we can post directly to _id we will not have to do this part anymore. - const signalUpdating = await updateIfIdExists({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - query, - language, - savedId, - filters, - id, - immutable, - index, - interval, - maxSignals, - name, - severity, - size, - tags, - to, - type, - references, - }); - if (signalUpdating == null) { - // TODO: Right now we are using the .server-log as the default action as each alert has to have - // at least one action or it will not be able to do in-memory persistence. When adding in actions - // such as email, slack, etc... this should be the default action if no action is specified to - // create signals - const actionResults = await actionsClient.create({ - action: { - actionTypeId: '.server-log', - description: 'SIEM Alerts Log', - config: {}, - secrets: {}, - }, - }); - - return alertsClient.create({ - data: { - name, - alertTypeId: SIGNALS_ID, - alertTypeParams: { - description, - id, - index, - falsePositives, - from, - filter, - immutable, - query, - language, - savedId, - filters, - maxSignals, - severity, - tags, - to, - type, - references, - }, - interval, - enabled, - actions: [ - { - group: 'default', - id: actionResults.id, - params: { - message: 'SIEM Alert Fired', - level: 'info', - }, - }, - ], - throttle: null, + return alertsClient.create({ + data: { + name, + alertTypeId: SIGNALS_ID, + alertTypeParams: { + description, + ruleId, + index, + falsePositives, + from, + filter, + immutable, + query, + language, + savedId, + filters, + maxSignals, + severity, + tags, + to, + type, + references, }, - }); - } else { - return signalUpdating; - } + interval, + enabled, + actions: [], // TODO: Create and add actions here once we have email, etc... + throttle: null, + }, + }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts index 7a69c11ecf2e59..d89895772f1efc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts @@ -4,35 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../alerting/server/types'; -import { ActionsClient } from '../../../../../actions/server/actions_client'; import { readSignals } from './read_signals'; import { DeleteSignalParams } from './types'; -export const deleteAllSignalActions = async ( - actionsClient: ActionsClient, - actions: AlertAction[] -): Promise => { - try { - await Promise.all(actions.map(async ({ id }) => actionsClient.delete({ id }))); +export const deleteSignals = async ({ + alertsClient, + actionsClient, // TODO: Use this when we have actions such as email, etc... + id, + ruleId, +}: DeleteSignalParams) => { + const signal = await readSignals({ alertsClient, id, ruleId }); + if (signal == null) { return null; - } catch (error) { - return error; } -}; - -export const deleteSignals = async ({ alertsClient, actionsClient, id }: DeleteSignalParams) => { - const signal = await readSignals({ alertsClient, id }); - // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed - // where it is trying to return AlertAction[] or RawAlertAction[] - const actions = (signal.actions as AlertAction[] | undefined) || []; - - const actionsErrors = await deleteAllSignalActions(actionsClient, actions); - const deletedAlert = await alertsClient.delete({ id: signal.id }); - if (actionsErrors != null) { - throw actionsErrors; + if (ruleId != null) { + await alertsClient.delete({ id: signal.id }); + return signal; + } else if (id != null) { + try { + await alertsClient.delete({ id }); + return signal; + } catch (err) { + if (err.output.statusCode === 404) { + return null; + } else { + throw err; + } + } } else { - return deletedAlert; + return null; } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts index ae79e9bc01480d..bc5d11d70586dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -42,13 +42,13 @@ export const getQueryFilter = ( interface GetFilterArgs { type: SignalAlertParams['type']; - filter: Record | undefined; - filters: PartialFilter[] | undefined; - language: string | undefined; - query: string | undefined; - savedId: string | undefined; + filter: Record | undefined | null; + filters: PartialFilter[] | undefined | null; + language: string | undefined | null; + query: string | undefined | null; + savedId: string | undefined | null; services: AlertServices; - index: string[] | undefined; + index: string[] | undefined | null; } export const getFilter = async ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts new file mode 100644 index 00000000000000..dde3f19b1c66dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { readSignals, readSignalByRuleId, findSignalInArrayByRuleId } from './read_signals'; +import { AlertsClient } from '../../../../../alerting'; +import { + getResult, + getFindResultWithSingleHit, + getFindResultWithMultiHits, +} from '../routes/__mocks__/request_responses'; +import { SIGNALS_ID } from '../../../../common/constants'; + +describe('read_signals', () => { + describe('readSignals', () => { + test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignals({ + alertsClient: unsafeCast, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: undefined, + }); + expect(signal).toEqual(getResult()); + }); + + test('should return the output from alertsClient if id is set but ruleId is null', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignals({ + alertsClient: unsafeCast, + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + ruleId: null, + }); + expect(signal).toEqual(getResult()); + }); + + test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignals({ + alertsClient: unsafeCast, + id: undefined, + ruleId: 'rule-1', + }); + expect(signal).toEqual(getResult()); + }); + + test('should return the output from alertsClient if id is null but ruleId is set', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignals({ + alertsClient: unsafeCast, + id: null, + ruleId: 'rule-1', + }); + expect(signal).toEqual(getResult()); + }); + + test('should return null if id and ruleId are null', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignals({ + alertsClient: unsafeCast, + id: null, + ruleId: null, + }); + expect(signal).toEqual(null); + }); + + test('should return null if id and ruleId are undefined', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignals({ + alertsClient: unsafeCast, + id: undefined, + ruleId: undefined, + }); + expect(signal).toEqual(null); + }); + }); + + describe('readSignalByRuleId', () => { + test('should return a single value if the rule id matches', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignalByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-1', + }); + expect(signal).toEqual(getResult()); + }); + + test('should not return a single value if the rule id does not match', async () => { + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignalByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-that-should-not-match-anything', + }); + expect(signal).toEqual(null); + }); + + test('should return a single value of rule-1 with multiple values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.alertTypeParams.ruleId = 'rule-1'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.alertTypeParams.ruleId = 'rule-2'; + + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignalByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-1', + }); + expect(signal).toEqual(result1); + }); + + test('should return a single value of rule-2 with multiple values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.alertTypeParams.ruleId = 'rule-1'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.alertTypeParams.ruleId = 'rule-2'; + + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignalByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-2', + }); + expect(signal).toEqual(result2); + }); + + test('should return null for a made up value with multiple values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.alertTypeParams.ruleId = 'rule-1'; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.alertTypeParams.ruleId = 'rule-2'; + + const alertsClient = alertsClientMock.create(); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const signal = await readSignalByRuleId({ + alertsClient: unsafeCast, + ruleId: 'rule-that-should-not-match-anything', + }); + expect(signal).toEqual(null); + }); + }); + + describe('findSignalInArrayByRuleId', () => { + test('returns null if the objects are not of a signal rule type', () => { + const signal = findSignalInArrayByRuleId( + [ + { alertTypeId: 'made up 1', alertTypeParams: { ruleId: '123' } }, + { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + ], + '123' + ); + expect(signal).toEqual(null); + }); + + test('returns correct type if the objects are of a signal rule type', () => { + const signal = findSignalInArrayByRuleId( + [ + { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, + { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + ], + '123' + ); + expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '123' } }); + }); + + test('returns second correct type if the objects are of a signal rule type', () => { + const signal = findSignalInArrayByRuleId( + [ + { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + ], + '456' + ); + expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '456' } }); + }); + + test('returns null with correct types but data does not exist', () => { + const signal = findSignalInArrayByRuleId( + [ + { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + ], + '892' + ); + expect(signal).toEqual(null); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts index 0ef13f39e793b6..f73074b560cb24 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts @@ -5,13 +5,16 @@ */ import { findSignals } from './find_signals'; -import { SignalAlertType, isAlertTypeArray, ReadSignalParams } from './types'; +import { SignalAlertType, isAlertTypeArray, ReadSignalParams, ReadSignalByRuleId } from './types'; -export const findSignalInArrayById = (objects: object[], id: string): SignalAlertType | null => { +export const findSignalInArrayByRuleId = ( + objects: object[], + ruleId: string +): SignalAlertType | null => { if (isAlertTypeArray(objects)) { const signals: SignalAlertType[] = objects; const signal: SignalAlertType[] = signals.filter(datum => { - return datum.alertTypeParams.id === id; + return datum.alertTypeParams.ruleId === ruleId; }); if (signal.length !== 0) { return signal[0]; @@ -28,12 +31,12 @@ export const findSignalInArrayById = (objects: object[], id: string): SignalAler // not indexed and I cannot push in my own _id when I create an alert at the moment. // TODO: Once we can directly push in the _id, then we should no longer need this way. // TODO: This is meant to be _very_ temporary. -export const readSignalById = async ({ +export const readSignalByRuleId = async ({ alertsClient, - id, -}: ReadSignalParams): Promise => { + ruleId, +}: ReadSignalByRuleId): Promise => { const firstSignals = await findSignals({ alertsClient, page: 1 }); - const firstSignal = findSignalInArrayById(firstSignals.data, id); + const firstSignal = findSignalInArrayByRuleId(firstSignals.data, ruleId); if (firstSignal != null) { return firstSignal; } else { @@ -46,7 +49,7 @@ export const readSignalById = async ({ }) .reduce>(async (accum, findSignal) => { const signals = await findSignal; - const signal = findSignalInArrayById(signals.data, id); + const signal = findSignalInArrayByRuleId(signals.data, ruleId); if (signal != null) { return signal; } else { @@ -56,11 +59,23 @@ export const readSignalById = async ({ } }; -export const readSignals = async ({ alertsClient, id }: ReadSignalParams) => { - const signalById = await readSignalById({ alertsClient, id }); - if (signalById != null) { - return signalById; +export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams) => { + if (id != null) { + try { + const output = await alertsClient.get({ id }); + return output; + } 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 (ruleId != null) { + return readSignalByRuleId({ alertsClient, ruleId }); } else { - return alertsClient.get({ id }); + // should never get here, and yet here we are. + return null; } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index be763f2b5386de..0f15cf8fcf5f4f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -26,7 +26,7 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), from: schema.string(), filter: schema.nullable(schema.object({}, { allowUnknowns: true })), - id: schema.string(), + ruleId: schema.string(), immutable: schema.boolean({ defaultValue: false }), index: schema.arrayOf(schema.string()), language: schema.nullable(schema.string()), @@ -42,19 +42,18 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp size: schema.maybe(schema.number()), }), }, - async executor({ services, params }) { + async executor({ alertId, services, params }) { const { description, filter, from, - id, + ruleId, index, filters, language, savedId, query, maxSignals, - name, references, severity, to, @@ -62,6 +61,9 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp size, } = params; + // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 + const savedObject = await services.savedObjectsClient.get('alert', alertId); + const name = savedObject.attributes.name; const searchAfterSize = size ? size : 1000; const esFilter = await getFilter({ @@ -85,7 +87,7 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp }); try { - logger.debug(`Starting signal rule "${id}"`); + logger.debug(`Starting signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); if (process.env.USE_REINDEX_API === 'true') { const reIndex = buildEventsReIndex({ index, @@ -101,22 +103,25 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp filter: esFilter, maxDocs: maxSignals, ruleRevision: 1, - id, + id: alertId, + ruleId, type, references, }); const result = await services.callCluster('reindex', reIndex); if (result.total > 0) { logger.info( - `Total signals found from signal rule "${id}" (reindex algorithm): ${result.total}` + `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}" (reindex algorithm): ${result.total}` ); } } else { - logger.debug(`[+] Initial search call of signal rule "${id}"`); + logger.debug( + `[+] Initial search call of signal rule "id: ${alertId}", "ruleId: ${ruleId}"` + ); const noReIndexResult = await services.callCluster('search', noReIndex); if (noReIndexResult.hits.total.value !== 0) { logger.info( - `Total signals found from signal rule "${id}": ${noReIndexResult.hits.total.value}` + `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}": ${noReIndexResult.hits.total.value}` ); } @@ -124,19 +129,22 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp noReIndexResult, params, services, - logger + logger, + alertId ); if (bulkIndexResult) { - logger.debug(`Finished signal rule "${id}"`); + logger.debug(`Finished signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); } else { - logger.error(`Error processing signal rule "${id}"`); + logger.error(`Error processing signal rule "id: ${alertId}", "ruleId: ${ruleId}"`); } } } catch (err) { // TODO: Error handling and writing of errors into a signal that has error // handling/conditions - logger.error(`Error from signal rule "${id}", ${err.message}`); + logger.error( + `Error from signal rule "id: ${alertId}", "ruleId: ${ruleId}", ${err.message}` + ); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 35561165859b1e..d7a02c6698b279 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -25,21 +25,21 @@ export interface SignalAlertParams { description: string; enabled: boolean; falsePositives: string[]; - filter: Record | undefined; - filters: PartialFilter[] | undefined; + filter: Record | undefined | null; + filters: PartialFilter[] | undefined | null; from: string; immutable: boolean; index: string[]; interval: string; - id: string; - language: string | undefined; + ruleId: string | undefined | null; + language: string | undefined | null; maxSignals: number; name: string; - query: string | undefined; + query: string | undefined | null; references: string[]; - savedId: string | undefined; + savedId: string | undefined | null; severity: string; - size: number | undefined; + size: number | undefined | null; tags: string[]; to: string; type: 'filter' | 'query' | 'saved_query'; @@ -47,15 +47,23 @@ export interface SignalAlertParams { export type SignalAlertParamsRest = Omit< SignalAlertParams, - 'falsePositives' | 'maxSignals' | 'saved_id' + 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' > & { + rule_id: SignalAlertParams['ruleId']; false_positives: SignalAlertParams['falsePositives']; saved_id: SignalAlertParams['savedId']; max_signals: SignalAlertParams['maxSignals']; }; -export type UpdateSignalAlertParamsRest = Partial> & { - id: SignalAlertParams['id']; +export type OutputSignalAlertRest = SignalAlertParamsRest & { + id: string; + created_by: string | undefined | null; + updated_by: string | undefined | null; +}; + +export type UpdateSignalAlertParamsRest = Partial & { + id: string | undefined; + rule_id: SignalAlertParams['ruleId'] | undefined; }; export interface FindParamsRest { @@ -72,11 +80,14 @@ export interface Clients { export type SignalParams = SignalAlertParams & Clients; -export type UpdateSignalParams = Partial> & { - id: SignalAlertParams['id']; +export type UpdateSignalParams = Partial & { + id: string | undefined | null; } & Clients; -export type DeleteSignalParams = Clients & { id: string }; +export type DeleteSignalParams = Clients & { + id: string | undefined; + ruleId: string | undefined | null; +}; export interface FindSignalsRequest extends Omit { query: { @@ -98,12 +109,20 @@ export interface FindSignalParams { export interface ReadSignalParams { alertsClient: AlertsClient; - id: string; + id?: string | undefined | null; + ruleId?: string | undefined | null; +} + +export interface ReadSignalByRuleId { + alertsClient: AlertsClient; + ruleId: string; } +export type AlertTypeParams = Omit; + export type SignalAlertType = Alert & { id: string; - alertTypeParams: SignalAlertParams; + alertTypeParams: AlertTypeParams; }; export interface SignalsRequest extends Hapi.Request { @@ -145,6 +164,10 @@ export interface BulkResponse { export type SignalSearchResponse = SearchResponse; export type SignalSourceHit = SignalSearchResponse['hits']['hits'][0]; +export type QueryRequest = Omit & { + query: { id: string | undefined; rule_id: string | undefined }; +}; + // This returns true because by default a SignalAlertTypeDefinition is an AlertType // since we are only increasing the strictness of params. export const isAlertExecutor = (obj: SignalAlertTypeDefinition): obj is AlertType => { @@ -155,6 +178,10 @@ export type SignalAlertTypeDefinition = Omit & { executor: ({ services, params, state }: SignalExecutorOptions) => Promise; }; +export const isAlertTypes = (obj: unknown[]): obj is SignalAlertType[] => { + return obj.every(signal => isAlertType(signal)); +}; + export const isAlertType = (obj: unknown): obj is SignalAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts index b4639ee5b9ee5e..cba5f0e31cfd4c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts @@ -55,6 +55,7 @@ export const updateSignal = async ({ from, immutable, id, + ruleId, index, interval, maxSignals, @@ -65,9 +66,10 @@ export const updateSignal = async ({ type, references, }: UpdateSignalParams) => { - // TODO: Error handling and abstraction. Right now if this is an error then what happens is we get the error of - // "message": "Saved object [alert/{id}] not found" - const signal = await readSignals({ alertsClient, id }); + const signal = await readSignals({ alertsClient, ruleId, id }); + if (signal == null) { + return null; + } // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed // where it is trying to return AlertAction[] or RawAlertAction[] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index c3ffb6e8c230af..a5e6d03a3378b6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -20,6 +20,7 @@ import { sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, + sampleSignalId, } from './__mocks__/es_results'; const mockLogger: Logger = { @@ -45,14 +46,15 @@ describe('utils', () => { describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { const sampleParams = sampleSignalAlertParams(undefined); - const fakeSignalSourceHit = buildBulkBody(sampleDocNoSortId, sampleParams); + const fakeSignalSourceHit = buildBulkBody(sampleDocNoSortId, sampleParams, sampleSignalId); expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', '@timestamp': 'someTimeStamp', signal: { + id: sampleSignalId, '@timestamp': fakeSignalSourceHit.signal['@timestamp'], // timestamp generated in the body rule_revision: 1, - rule_id: sampleParams.id, + rule_id: sampleParams.ruleId, rule_type: sampleParams.type, parent: { id: sampleDocNoSortId._id, @@ -87,7 +89,8 @@ describe('utils', () => { sampleSearchResult, sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(successfulSingleBulkIndex).toEqual(true); }); @@ -99,7 +102,8 @@ describe('utils', () => { sampleSearchResult, sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(successfulSingleBulkIndex).toEqual(true); }); @@ -115,7 +119,8 @@ describe('utils', () => { sampleSearchResult, sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(mockLogger.error).toHaveBeenCalled(); expect(successfulSingleBulkIndex).toEqual(false); @@ -160,7 +165,8 @@ describe('utils', () => { sampleEmptyDocSearchResults, sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(result).toEqual(true); @@ -201,7 +207,8 @@ describe('utils', () => { repeatedSearchResultsWithSortId(4), sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(result).toEqual(true); @@ -216,7 +223,8 @@ describe('utils', () => { repeatedSearchResultsWithSortId(4), sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); @@ -237,7 +245,8 @@ describe('utils', () => { sampleDocSearchResultsNoSortId, sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); @@ -257,7 +266,8 @@ describe('utils', () => { sampleDocSearchResultsNoSortIdNoHits, sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(result).toEqual(true); }); @@ -278,7 +288,8 @@ describe('utils', () => { repeatedSearchResultsWithSortId(4), sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(result).toEqual(true); }); @@ -300,7 +311,8 @@ describe('utils', () => { repeatedSearchResultsWithSortId(4), sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(true); @@ -322,7 +334,8 @@ describe('utils', () => { repeatedSearchResultsWithSortId(4), sampleParams, mockService, - mockLogger + mockLogger, + sampleSignalId ); expect(result).toEqual(false); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 509181f915f554..2967f41ffb697a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -12,13 +12,18 @@ import { SignalSourceHit, SignalSearchResponse, SignalAlertParams, BulkResponse import { buildEventsSearchQuery } from './build_events_query'; // format search_after result for signals index. -export const buildBulkBody = (doc: SignalSourceHit, signalParams: SignalAlertParams): SignalHit => { +export const buildBulkBody = ( + doc: SignalSourceHit, + signalParams: SignalAlertParams, + id: string +): SignalHit => { return { ...doc._source, signal: { '@timestamp': new Date().toISOString(), + id, rule_revision: 1, - rule_id: signalParams.id, + rule_id: signalParams.ruleId, rule_type: signalParams.type, parent: { id: doc._id, @@ -41,7 +46,8 @@ export const singleBulkIndex = async ( sr: SignalSearchResponse, params: SignalAlertParams, service: AlertServices, - logger: Logger + logger: Logger, + id: string ): Promise => { if (sr.hits.hits.length === 0) { return true; @@ -53,7 +59,7 @@ export const singleBulkIndex = async ( _id: doc._id, }, }, - buildBulkBody(doc, params), + buildBulkBody(doc, params, id), ]); const time1 = performance.now(); const firstResult: BulkResponse = await service.callCluster('bulk', { @@ -106,14 +112,15 @@ export const searchAfterAndBulkIndex = async ( someResult: SignalSearchResponse, params: SignalAlertParams, service: AlertServices, - logger: Logger + logger: Logger, + id: string ): Promise => { if (someResult.hits.hits.length === 0) { return true; } logger.debug('[+] starting bulk insertion'); - const firstBulkIndexSuccess = await singleBulkIndex(someResult, params, service, logger); + const firstBulkIndexSuccess = await singleBulkIndex(someResult, params, service, logger, id); if (!firstBulkIndexSuccess) { logger.error('First bulk index was unsuccessful'); return false; @@ -160,7 +167,7 @@ export const searchAfterAndBulkIndex = async ( } sortId = sortIds[0]; logger.debug('next bulk index'); - const bulkSuccess = await singleBulkIndex(searchAfterResult, params, service, logger); + const bulkSuccess = await singleBulkIndex(searchAfterResult, params, service, logger, id); logger.debug('finished next bulk index'); if (!bulkSuccess) { logger.error('[-] bulk index failed but continuing'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 7b3778d606e473..4c2f6a7592a17d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,13 +6,14 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalAlertParamsRest } from '../../alerts/types'; +import { SignalAlertParamsRest, SignalAlertType } from '../../alerts/types'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point // when we upgrade and Hapi Server is ok with the filter. export const typicalPayload = (): Partial> => ({ - id: 'rule-1', + rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', @@ -25,9 +26,22 @@ export const typicalPayload = (): Partial> language: 'kuery', }); +export const typicalFilterPayload = (): Partial => ({ + rule_id: 'rule-1', + description: 'Detecting root and admin users', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + name: 'Detect Root/Admin Users', + type: 'filter', + from: 'now-6m', + to: 'now', + severity: 'high', + filter: {}, +}); + export const getUpdateRequest = (): ServerInjectOptions => ({ method: 'PUT', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { ...typicalPayload(), }, @@ -35,29 +49,55 @@ export const getUpdateRequest = (): ServerInjectOptions => ({ export const getReadRequest = (): ServerInjectOptions => ({ method: 'GET', - url: '/api/siem/signals/rule-1', + url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`, }); export const getFindRequest = (): ServerInjectOptions => ({ method: 'GET', - url: '/api/siem/signals/_find', + url: `${DETECTION_ENGINE_RULES_URL}/_find`, }); -export const getFindResult = () => ({ +interface FindHit { + page: number; + perPage: number; + total: number; + data: SignalAlertType[]; +} + +export const getFindResult = (): FindHit => ({ page: 1, perPage: 1, total: 0, data: [], }); +export const getFindResultWithSingleHit = (): FindHit => ({ + page: 1, + perPage: 1, + total: 0, + data: [getResult()], +}); + +export const getFindResultWithMultiHits = (data: SignalAlertType[]): FindHit => ({ + page: 1, + perPage: 1, + total: 2, + data, +}); + export const getDeleteRequest = (): ServerInjectOptions => ({ method: 'DELETE', - url: '/api/siem/signals/rule-1', + url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`, +}); + +export const getDeleteRequestById = (): ServerInjectOptions => ({ + method: 'DELETE', + url: `${DETECTION_ENGINE_RULES_URL}?id=04128c15-0d1b-4716-a4c5-46997ac7f3bd`, }); export const getCreateRequest = (): ServerInjectOptions => ({ method: 'POST', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { ...typicalPayload(), }, @@ -70,52 +110,40 @@ export const createActionResult = (): ActionResult => ({ config: {}, }); -export const createAlertResult = () => ({ - id: 'rule-1', +export const getResult = (): SignalAlertType => ({ + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + name: 'Detect Root/Admin Users', alertTypeId: 'siem.signals', alertTypeParams: { description: 'Detecting root and admin users', - id: 'rule-1', + ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + falsePositives: [], from: 'now-6m', - filter: null, + filter: undefined, + immutable: false, query: 'user.name: root or user.name: admin', + language: 'kuery', + savedId: undefined, + filters: undefined, maxSignals: 100, - name: 'Detect Root/Admin Users', + size: 1, severity: 'high', + tags: [], to: 'now', type: 'query', - language: 'kuery', - references: [], + references: ['http://www.example.com', 'https://ww.example.com'], }, interval: '5m', enabled: true, - actions: [ - { - group: 'default', - params: { - message: 'SIEM Alert Fired', - level: 'info', - }, - id: '9c3846a3-dbf9-40ce-ba7e-ef635499afa6', - }, - ], + actions: [], throttle: null, createdBy: 'elastic', updatedBy: 'elastic', apiKeyOwner: 'elastic', muteAll: false, mutedInstanceIds: [], - scheduledTaskId: '78d036d0-f042-11e9-a9ae-51b9a11630ec', -}); - -export const getResult = () => ({ - id: 'result-1', - enabled: false, - alertTypeId: '', - interval: undefined, - actions: undefined, - alertTypeParams: undefined, + scheduledTaskId: '2dabe330-0702-11ea-8b50-773b89126888', }); export const updateActionResult = (): ActionResult => ({ @@ -124,42 +152,3 @@ export const updateActionResult = (): ActionResult => ({ description: '', config: {}, }); - -export const updateAlertResult = () => ({ - id: 'rule-1', - alertTypeId: 'siem.signals', - alertTypeParams: { - description: 'Detecting root and admin users', - id: 'rule-1', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - from: 'now-6m', - filter: null, - query: 'user.name: root or user.name: admin', - maxSignals: 100, - name: 'Detect Root/Admin Users', - severity: 'high', - to: 'now', - type: 'query', - language: 'kuery', - references: [], - }, - interval: '5m', - enabled: true, - actions: [ - { - group: 'default', - params: { - message: 'SIEM Alert Fired', - level: 'info', - }, - id: '9c3846a3-dbf9-40ce-ba7e-ef635499afa6', - }, - ], - throttle: null, - createdBy: 'elastic', - updatedBy: 'elastic', - apiKeyOwner: 'elastic', - muteAll: false, - mutedInstanceIds: [], - scheduledTaskId: '78d036d0-f042-11e9-a9ae-51b9a11630ec', -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts index 0fe88cc856d622..1232fe3ce219d3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts @@ -16,10 +16,10 @@ import { getFindResult, getResult, createActionResult, - createAlertResult, getCreateRequest, typicalPayload, } from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('create_signals', () => { let { server, alertsClient, actionsClient } = createMockServer(); @@ -35,7 +35,7 @@ describe('create_signals', () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(createAlertResult()); + alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getCreateRequest()); expect(statusCode).toBe(200); }); @@ -65,31 +65,31 @@ describe('create_signals', () => { }); describe('validation', () => { - test('returns 400 if id is not given', async () => { + test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(createAlertResult()); - // missing id should throw a 400 - const { id, ...noId } = typicalPayload(); + alertsClient.create.mockResolvedValue(getResult()); + // missing rule_id should return 200 as it will be auto generated if not given + const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', - url: '/api/siem/signals', - payload: noId, + url: DETECTION_ENGINE_RULES_URL, + payload: noRuleId, }; const { statusCode } = await server.inject(request); - expect(statusCode).toBe(400); + expect(statusCode).toBe(200); }); test('returns 200 if type is query', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(createAlertResult()); + alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { ...noType, type: 'query', @@ -103,13 +103,13 @@ describe('create_signals', () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(createAlertResult()); + alertsClient.create.mockResolvedValue(getResult()); // Cannot type request with a ServerInjectOptions as the type system complains // about the property filter involving Hapi types, so I left it off for now const { language, query, type, ...noType } = typicalPayload(); const request = { method: 'POST', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { ...noType, type: 'filter', @@ -124,11 +124,11 @@ describe('create_signals', () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(createAlertResult()); + alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { ...noType, type: 'something-made-up', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts index d3474f214194c3..856866fb443ba1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts @@ -6,13 +6,18 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; +import Boom from 'boom'; +import uuid from 'uuid'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { createSignals } from '../alerts/create_signals'; import { SignalsRequest } from '../alerts/types'; import { createSignalsSchema } from './schemas'; +import { readSignals } from '../alerts/read_signals'; +import { transformOrError } from './utils'; export const createCreateSignalsRoute: Hapi.ServerRoute = { method: 'POST', - path: '/api/siem/signals', + path: DETECTION_ENGINE_RULES_URL, options: { tags: ['access:signals-all'], validate: { @@ -36,7 +41,8 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { // eslint-disable-next-line @typescript-eslint/camelcase saved_id: savedId, filters, - id, + // eslint-disable-next-line @typescript-eslint/camelcase + rule_id: ruleId, index, interval, // eslint-disable-next-line @typescript-eslint/camelcase @@ -57,7 +63,13 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - return createSignals({ + if (ruleId != null) { + const signal = await readSignals({ alertsClient, ruleId }); + if (signal != null) { + return new Boom(`Signal rule_id ${ruleId} already exists`, { statusCode: 409 }); + } + } + const createdSignal = await createSignals({ alertsClient, actionsClient, description, @@ -70,7 +82,7 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { language, savedId, filters, - id, + ruleId: ruleId != null ? ruleId : uuid.v4(), index, interval, maxSignals, @@ -82,6 +94,7 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { type, references, }); + return transformOrError(createdSignal); }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts index db74cf6508be60..95816aa55d1fea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts @@ -13,7 +13,14 @@ import { import { deleteSignalsRoute } from './delete_signals_route'; import { ServerInjectOptions } from 'hapi'; -import { getFindResult, getResult, getDeleteRequest } from './__mocks__/request_responses'; +import { + getFindResult, + getResult, + getDeleteRequest, + getFindResultWithSingleHit, + getDeleteRequestById, +} from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('delete_signals', () => { let { server, alertsClient } = createMockServer(); @@ -28,14 +35,30 @@ describe('delete_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); + test('returns 200 when deleting a single signal with a valid actionClient and alertClient by alertId', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(200); }); + test('returns 200 when deleting a single signal with a valid actionClient and alertClient by id', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteRequestById()); + expect(statusCode).toBe(200); + }); + + test('returns 404 when deleting a single signal that does not exist with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteRequest()); + expect(statusCode).toBe(404); + }); + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); deleteSignalsRoute(serverWithoutActionClient); @@ -61,16 +84,16 @@ describe('delete_signals', () => { }); describe('validation', () => { - test('returns 404 if given a non-existent id', async () => { + test('returns 400 if given a non-existent id', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, }; const { statusCode } = await server.inject(request); - expect(statusCode).toBe(404); + expect(statusCode).toBe(400); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts index d89d996eb06a2c..46565bc9950a52 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts @@ -7,21 +7,26 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { deleteSignals } from '../alerts/delete_signals'; +import { querySignalSchema } from './schemas'; +import { QueryRequest } from '../alerts/types'; +import { getIdError, transformOrError } from './utils'; export const createDeleteSignalsRoute: Hapi.ServerRoute = { method: 'DELETE', - path: '/api/siem/signals/{id}', + path: DETECTION_ENGINE_RULES_URL, options: { tags: ['access:signals-all'], validate: { options: { abortEarly: false, }, + query: querySignalSchema, }, }, - async handler(request: Hapi.Request, headers) { - const { id } = request.params; + async handler(request: QueryRequest, headers) { + const { id, rule_id: ruleId } = request.query; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; @@ -29,11 +34,18 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - return deleteSignals({ + const signal = await deleteSignals({ actionsClient, alertsClient, id, + ruleId, }); + + if (signal != null) { + return transformOrError(signal); + } else { + return getIdError({ id, ruleId }); + } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts index 331f8874eb29b5..be3dce36e87167 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts @@ -14,6 +14,7 @@ import { import { findSignalsRoute } from './find_signals_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('find_signals', () => { let { server, alertsClient, actionsClient } = createMockServer(); @@ -71,7 +72,7 @@ describe('find_signals', () => { alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', - url: '/api/siem/signals/_find?invalid_value=500', + url: `${DETECTION_ENGINE_RULES_URL}/_find?invalid_value=500`, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); @@ -82,8 +83,7 @@ describe('find_signals', () => { alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', - url: - '/api/siem/signals/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]', + url: `${DETECTION_ENGINE_RULES_URL}/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]`, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts index e6f4703ff2e635..6be2c145edc6ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts @@ -6,13 +6,15 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { findSignals } from '../alerts/find_signals'; import { FindSignalsRequest } from '../alerts/types'; import { findSignalsSchema } from './schemas'; +import { transformFindAlertsOrError } from './utils'; export const createFindSignalRoute: Hapi.ServerRoute = { method: 'GET', - path: '/api/siem/signals/_find', + path: `${DETECTION_ENGINE_RULES_URL}/_find`, options: { tags: ['access:signals-all'], validate: { @@ -31,12 +33,13 @@ export const createFindSignalRoute: Hapi.ServerRoute = { return headers.response().code(404); } - return findSignals({ + const signals = await findSignals({ alertsClient, perPage: query.per_page, page: query.page, sortField: query.sort_field, }); + return transformFindAlertsOrError(signals); }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts index 43c96792606a24..021bcc7b8b48e0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts @@ -13,7 +13,13 @@ import { import { readSignalsRoute } from './read_signals_route'; import { ServerInjectOptions } from 'hapi'; -import { getFindResult, getResult, getReadRequest } from './__mocks__/request_responses'; +import { + getFindResult, + getResult, + getReadRequest, + getFindResultWithSingleHit, +} from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('read_signals', () => { let { server, alertsClient } = createMockServer(); @@ -29,7 +35,7 @@ describe('read_signals', () => { describe('status codes with actionClient and alertClient', () => { test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadRequest()); expect(statusCode).toBe(200); @@ -60,16 +66,16 @@ describe('read_signals', () => { }); describe('validation', () => { - test('returns 404 if given a non-existent id', async () => { + test('returns 400 if given a non-existent id', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'GET', - url: '/api/siem/signals/', + url: DETECTION_ENGINE_RULES_URL, }; const { statusCode } = await server.inject(request); - expect(statusCode).toBe(404); + expect(statusCode).toBe(400); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts index b26c8c17f32dce..71a1afc921befb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts @@ -6,32 +6,43 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { getIdError, transformOrError } from './utils'; import { readSignals } from '../alerts/read_signals'; +import { querySignalSchema } from './schemas'; +import { QueryRequest } from '../alerts/types'; export const createReadSignalsRoute: Hapi.ServerRoute = { method: 'GET', - path: '/api/siem/signals/{id}', + path: DETECTION_ENGINE_RULES_URL, options: { tags: ['access:signals-all'], validate: { options: { abortEarly: false, }, + query: querySignalSchema, }, }, - async handler(request: Hapi.Request, headers) { - const { id } = request.params; + async handler(request: QueryRequest, headers) { + const { id, rule_id: ruleId } = request.query; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; if (!alertsClient || !actionsClient) { return headers.response().code(404); } - return readSignals({ + const signal = await readSignals({ alertsClient, id, + ruleId, }); + if (signal != null) { + return transformOrError(signal); + } else { + return getIdError({ id, ruleId }); + } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 35432c2bf56b58..ecb42399932f6b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,10 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createSignalsSchema, updateSignalSchema, findSignalsSchema } from './schemas'; -import { SignalAlertParamsRest, FindParamsRest } from '../alerts/types'; - -describe('update_signals', () => { +import { + createSignalsSchema, + updateSignalSchema, + findSignalsSchema, + querySignalSchema, +} from './schemas'; +import { + SignalAlertParamsRest, + FindParamsRest, + UpdateSignalAlertParamsRest, +} from '../alerts/types'; + +describe('schemas', () => { describe('create signals schema', () => { test('empty objects do not validate', () => { expect(createSignalsSchema.validate>({}).error).toBeTruthy(); @@ -21,37 +30,37 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id] does not validate', () => { + test('[rule_id] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', }).error ).toBeTruthy(); }); - test('[id, description] does not validate', () => { + test('[rule_id, description] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', }).error ).toBeTruthy(); }); - test('[id, description, from] does not validate', () => { + test('[rule_id, description, from] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', }).error ).toBeTruthy(); }); - test('[id, description, from, to] does not validate', () => { + test('[rule_id, description, from, to] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -59,10 +68,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, name] does not validate', () => { + test('[rule_id, description, from, to, name] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -71,10 +80,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, name, severity] does not validate', () => { + test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -84,10 +93,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type] does not validate', () => { + test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -98,10 +107,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type, interval] does not validate', () => { + test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -113,10 +122,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type, interval, index] does not validate', () => { + test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -129,10 +138,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -146,10 +155,10 @@ describe('update_signals', () => { ).toBeTruthy(); }); - test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -164,10 +173,10 @@ describe('update_signals', () => { ).toBeFalsy(); }); - test('[id, description, from, to, index, name, severity, interval, type, filter] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter] does validate', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -184,7 +193,7 @@ describe('update_signals', () => { test('If filter type is set then filter is required', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -200,7 +209,7 @@ describe('update_signals', () => { test('If filter type is set then query is not allowed', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -218,7 +227,7 @@ describe('update_signals', () => { test('If filter type is set then language is not allowed', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -236,7 +245,7 @@ describe('update_signals', () => { test('If filter type is set then filters are not allowed', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -254,7 +263,7 @@ describe('update_signals', () => { test('allows references to be sent as valid', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -273,7 +282,7 @@ describe('update_signals', () => { test('defaults references to an array', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -293,7 +302,7 @@ describe('update_signals', () => { createSignalsSchema.validate< Partial> & { references: number[] } >({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -314,7 +323,7 @@ describe('update_signals', () => { createSignalsSchema.validate< Partial> & { index: number[] } >({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -332,7 +341,7 @@ describe('update_signals', () => { test('defaults interval to 5 min', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -347,7 +356,7 @@ describe('update_signals', () => { test('defaults max signals to 100', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -363,7 +372,7 @@ describe('update_signals', () => { test('filter and filters cannot exist together', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -381,7 +390,7 @@ describe('update_signals', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -397,7 +406,7 @@ describe('update_signals', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -414,7 +423,7 @@ describe('update_signals', () => { test('saved_query type cannot have filters with it', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -432,7 +441,7 @@ describe('update_signals', () => { test('saved_query type cannot have filter with it', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -450,7 +459,7 @@ describe('update_signals', () => { test('language validates with kuery', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -469,7 +478,7 @@ describe('update_signals', () => { test('language validates with lucene', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -488,7 +497,7 @@ describe('update_signals', () => { test('language does not validate with something made up', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -507,7 +516,7 @@ describe('update_signals', () => { test('max_signals cannot be negative', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -527,7 +536,7 @@ describe('update_signals', () => { test('max_signals cannot be zero', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -547,7 +556,7 @@ describe('update_signals', () => { test('max_signals can be 1', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -567,7 +576,7 @@ describe('update_signals', () => { test('You can optionally send in an array of tags', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -590,7 +599,7 @@ describe('update_signals', () => { createSignalsSchema.validate< Partial> & { tags: number[] } >({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -611,7 +620,7 @@ describe('update_signals', () => { test('You can optionally send in an array of false positives', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', false_positives: ['false_1', 'false_2'], from: 'now-5m', @@ -634,7 +643,7 @@ describe('update_signals', () => { createSignalsSchema.validate< Partial> & { false_positives: number[] } >({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', false_positives: [5, 4], from: 'now-5m', @@ -655,7 +664,7 @@ describe('update_signals', () => { test('You can optionally set the immutable to be true', () => { expect( createSignalsSchema.validate>({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -678,7 +687,7 @@ describe('update_signals', () => { createSignalsSchema.validate< Partial> & { immutable: number } >({ - id: 'rule-1', + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -698,13 +707,15 @@ describe('update_signals', () => { }); describe('update signals schema', () => { - test('empty objects do validate', () => { - expect(updateSignalSchema.validate>({}).error).toBeFalsy(); + test('empty objects do not validate as they require at least id or rule_id', () => { + expect( + updateSignalSchema.validate>({}).error + ).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -712,24 +723,60 @@ describe('update_signals', () => { test('[id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', }).error ).toBeFalsy(); }); + test('[rule_id] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[id and rule_id] does not validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'id-1', + rule_id: 'rule-1', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + test('[id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', }).error ).toBeFalsy(); }); + test('[rule_id, description, from] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + test('[id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -737,9 +784,20 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -748,9 +806,21 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, name] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -760,9 +830,22 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, name, severity] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -773,9 +856,23 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, name, severity, type] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -787,9 +884,24 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -802,9 +914,25 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -818,9 +946,26 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -835,9 +980,27 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -853,9 +1016,26 @@ describe('update_signals', () => { ).toBeFalsy(); }); + test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + updateSignalSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + }).error + ).toBeFalsy(); + }); + test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -872,7 +1052,7 @@ describe('update_signals', () => { test('If filter type is set then filter is still not required', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -888,7 +1068,7 @@ describe('update_signals', () => { test('If filter type is set then query is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -906,7 +1086,7 @@ describe('update_signals', () => { test('If filter type is set then language is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -924,7 +1104,7 @@ describe('update_signals', () => { test('If filter type is set then filters are not allowed', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -942,7 +1122,7 @@ describe('update_signals', () => { test('allows references to be sent as a valid value to update with', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -961,7 +1141,7 @@ describe('update_signals', () => { test('does not default references to an array', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -979,7 +1159,7 @@ describe('update_signals', () => { test('does not default interval', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -994,7 +1174,7 @@ describe('update_signals', () => { test('does not default max signal', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1011,7 +1191,7 @@ describe('update_signals', () => { test('references cannot be numbers', () => { expect( updateSignalSchema.validate< - Partial> & { references: number[] } + Partial> & { references: number[] } >({ id: 'rule-1', description: 'some description', @@ -1032,7 +1212,7 @@ describe('update_signals', () => { test('indexes cannot be numbers', () => { expect( updateSignalSchema.validate< - Partial> & { index: number[] } + Partial> & { index: number[] } >({ id: 'rule-1', description: 'some description', @@ -1051,7 +1231,7 @@ describe('update_signals', () => { test('filter and filters cannot exist together', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1069,7 +1249,7 @@ describe('update_signals', () => { test('saved_id is not required when type is saved_query and will validate without it', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1085,7 +1265,7 @@ describe('update_signals', () => { test('saved_id validates with saved_query', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1102,7 +1282,7 @@ describe('update_signals', () => { test('saved_query type cannot have filters with it', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1120,7 +1300,7 @@ describe('update_signals', () => { test('saved_query type cannot have filter with it', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1138,7 +1318,7 @@ describe('update_signals', () => { test('language validates with kuery', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1157,7 +1337,7 @@ describe('update_signals', () => { test('language validates with lucene', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1176,7 +1356,7 @@ describe('update_signals', () => { test('language does not validate with something made up', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1195,7 +1375,7 @@ describe('update_signals', () => { test('max_signals cannot be negative', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1215,7 +1395,7 @@ describe('update_signals', () => { test('max_signals cannot be zero', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1235,7 +1415,7 @@ describe('update_signals', () => { test('max_signals can be 1', () => { expect( - updateSignalSchema.validate>({ + updateSignalSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1326,4 +1506,31 @@ describe('update_signals', () => { expect(findSignalsSchema.validate>({}).value.page).toEqual(1); }); }); + + describe('querySignalSchema', () => { + test('empty objects do not validate', () => { + expect( + querySignalSchema.validate>({}).error + ).toBeTruthy(); + }); + + test('both rule_id and id being supplied dot not validate', () => { + expect( + querySignalSchema.validate>({ rule_id: '1', id: '1' }) + .error + ).toBeTruthy(); + }); + + test('only id validates', () => { + expect( + querySignalSchema.validate>({ id: '1' }).error + ).toBeFalsy(); + }); + + test('only rule_id validates', () => { + expect( + querySignalSchema.validate>({ rule_id: '1' }).error + ).toBeFalsy(); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index ba2c74b8bf0a92..596850b4a11e4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -14,6 +14,7 @@ const filter = Joi.object(); const filters = Joi.array(); const from = Joi.string(); const immutable = Joi.boolean(); +const rule_id = Joi.string(); const id = Joi.string(); const index = Joi.array() .items(Joi.string()) @@ -50,7 +51,7 @@ export const createSignalsSchema = Joi.object({ filter: filter.when('type', { is: 'filter', then: Joi.required(), otherwise: Joi.forbidden() }), filters: filters.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), from: from.required(), - id: id.required(), + rule_id, immutable: immutable.default(false), index: index.required(), interval: interval.default('5m'), @@ -81,6 +82,7 @@ export const updateSignalSchema = Joi.object({ filter: filter.when('type', { is: 'filter', then: Joi.optional(), otherwise: Joi.forbidden() }), filters: filters.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), from, + rule_id, id, immutable, index, @@ -103,7 +105,12 @@ export const updateSignalSchema = Joi.object({ to, type, references, -}); +}).xor('id', 'rule_id'); + +export const querySignalSchema = Joi.object({ + rule_id, + id, +}).xor('id', 'rule_id'); export const findSignalsSchema = Joi.object({ per_page, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts index c553a8bd40973f..7288d18628316b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts @@ -17,10 +17,12 @@ import { getFindResult, getResult, updateActionResult, - updateAlertResult, getUpdateRequest, typicalPayload, + getFindResultWithSingleHit, + typicalFilterPayload, } from './__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; describe('update_signals', () => { let { server, alertsClient, actionsClient } = createMockServer(); @@ -33,14 +35,23 @@ describe('update_signals', () => { describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(updateAlertResult()); + alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(200); }); + test('returns 404 when updating a single signal that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getUpdateRequest()); + expect(statusCode).toBe(404); + }); + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); updateSignalsRoute(serverWithoutActionClient); @@ -67,12 +78,12 @@ describe('update_signals', () => { describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); - const { id, ...noId } = typicalPayload(); + const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { payload: noId, }, @@ -81,51 +92,56 @@ describe('update_signals', () => { expect(statusCode).toBe(400); }); - test('returns 200 if type is query', async () => { + test('returns 404 if the record does not exist yet', async () => { alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(updateAlertResult()); - const { type, ...noType } = typicalPayload(); + alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', - url: '/api/siem/signals', - payload: { - ...noType, - type: 'query', - }, + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); }); test('returns 200 if type is filter', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(updateAlertResult()); - const { language, query, type, ...noType } = typicalPayload(); + alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', - url: '/api/siem/signals', - payload: { - ...noType, - type: 'filter', - }, + url: DETECTION_ENGINE_RULES_URL, + payload: typicalFilterPayload(), }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(updateAlertResult()); + alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', - url: '/api/siem/signals', + url: DETECTION_ENGINE_RULES_URL, payload: { ...noType, type: 'something-made-up', @@ -134,21 +150,5 @@ describe('update_signals', () => { const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); - - test('returns 200 if id is given in the url but not the payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(updateAlertResult()); - // missing id should throw a 400 - const { id, ...noId } = typicalPayload(); - const request: ServerInjectOptions = { - method: 'PUT', - url: '/api/siem/signals/rule-1', - payload: noId, - }; - const { statusCode } = await server.inject(request); - expect(statusCode).toBe(200); - }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts index a1c5af1158a479..9852fd0c5d271e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts @@ -5,28 +5,22 @@ */ import Hapi from 'hapi'; -import Joi from 'joi'; import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { updateSignal } from '../alerts/update_signals'; import { UpdateSignalsRequest } from '../alerts/types'; import { updateSignalSchema } from './schemas'; +import { getIdError, transformOrError } from './utils'; export const createUpdateSignalsRoute: Hapi.ServerRoute = { method: 'PUT', - path: '/api/siem/signals/{id?}', + path: DETECTION_ENGINE_RULES_URL, options: { tags: ['access:signals-all'], validate: { options: { abortEarly: false, }, - params: { - id: Joi.when(Joi.ref('$payload.id'), { - is: Joi.exist(), - then: Joi.string().optional(), - otherwise: Joi.string().required(), - }), - }, payload: updateSignalSchema, }, }, @@ -43,6 +37,8 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { // eslint-disable-next-line @typescript-eslint/camelcase saved_id: savedId, filters, + // eslint-disable-next-line @typescript-eslint/camelcase + rule_id: ruleId, id, index, interval, @@ -63,7 +59,8 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { if (!alertsClient || !actionsClient) { return headers.response().code(404); } - return updateSignal({ + + const signal = await updateSignal({ alertsClient, actionsClient, description, @@ -76,7 +73,8 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { language, savedId, filters, - id: request.params.id ? request.params.id : id, + id, + ruleId, index, interval, maxSignals, @@ -88,6 +86,11 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { type, references, }); + if (signal != null) { + return transformOrError(signal); + } else { + return getIdError({ id, ruleId }); + } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts new file mode 100644 index 00000000000000..69f25e84d995ca --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -0,0 +1,268 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { + transformAlertToSignal, + getIdError, + transformFindAlertsOrError, + transformOrError, +} from './utils'; +import { getResult } from './__mocks__/request_responses'; + +describe('utils', () => { + describe('transformAlertToSignal', () => { + test('should work with a full data set', () => { + const fullSignal = getResult(); + const signal = transformAlertToSignal(fullSignal); + expect(signal).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('should work with a partial data set missing data', () => { + const fullSignal = getResult(); + const { from, language, ...omitData } = transformAlertToSignal(fullSignal); + expect(omitData).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('should omit query if query is null', () => { + const fullSignal = getResult(); + fullSignal.alertTypeParams.query = null; + const signal = transformAlertToSignal(fullSignal); + expect(signal).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('should omit query if query is undefined', () => { + const fullSignal = getResult(); + fullSignal.alertTypeParams.query = undefined; + const signal = transformAlertToSignal(fullSignal); + expect(signal).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('should omit a mix of undefined, null, and missing fields', () => { + const fullSignal = getResult(); + fullSignal.alertTypeParams.query = undefined; + fullSignal.alertTypeParams.language = null; + const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); + expect(omitData).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + }); + + describe('getIdError', () => { + test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { + const boom = getIdError({ id: '123', ruleId: undefined }); + expect(boom.message).toEqual('id of 123 not found'); + }); + + test('outputs message about id not being found if only id is defined and ruleId is null', () => { + const boom = getIdError({ id: '123', ruleId: null }); + expect(boom.message).toEqual('id of 123 not found'); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { + const boom = getIdError({ id: undefined, ruleId: 'rule-id-123' }); + expect(boom.message).toEqual('rule_id of rule-id-123 not found'); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { + const boom = getIdError({ id: null, ruleId: 'rule-id-123' }); + expect(boom.message).toEqual('rule_id of rule-id-123 not found'); + }); + + test('outputs message about both being not defined when both are undefined', () => { + const boom = getIdError({ id: undefined, ruleId: undefined }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + + test('outputs message about both being not defined when both are null', () => { + const boom = getIdError({ id: null, ruleId: null }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + + test('outputs message about both being not defined when id is null and ruleId is undefined', () => { + const boom = getIdError({ id: null, ruleId: undefined }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + + test('outputs message about both being not defined when id is undefined and ruleId is null', () => { + const boom = getIdError({ id: undefined, ruleId: null }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + }); + + describe('transformFindAlertsOrError', () => { + test('outputs empty data set when data set is empty correct', () => { + const output = transformFindAlertsOrError({ data: [] }); + expect(output).toEqual({ data: [] }); + }); + + test('outputs 200 if the data is of type siem alert', () => { + const output = transformFindAlertsOrError({ + data: [getResult()], + }); + expect(output).toEqual({ + data: [ + { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + ], + }); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const output = transformFindAlertsOrError({ data: [{ random: 1 }] }); + expect((output as Boom).message).toEqual('Internal error transforming'); + }); + }); + + describe('transformOrError', () => { + test('outputs 200 if the data is of type siem alert', () => { + const output = transformOrError(getResult()); + expect(output).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + size: 1, + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const output = transformOrError({ data: [{ random: 1 }] }); + expect((output as Boom).message).toEqual('Internal error transforming'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts new file mode 100644 index 00000000000000..4d653210b2bffb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pickBy, identity } from 'lodash/fp'; +import { SignalAlertType, isAlertType, OutputSignalAlertRest, isAlertTypes } from '../alerts/types'; + +export const getIdError = ({ + id, + ruleId, +}: { + id: string | undefined | null; + ruleId: string | undefined | null; +}) => { + if (id != null) { + return new Boom(`id of ${id} not found`, { statusCode: 404 }); + } else if (ruleId != null) { + return new Boom(`rule_id of ${ruleId} not found`, { statusCode: 404 }); + } else { + return new Boom(`id or rule_id should have been defined`, { statusCode: 404 }); + } +}; + +// Transforms the data but will remove any null or undefined it encounters and not include +// those on the export +export const transformAlertToSignal = (signal: SignalAlertType): Partial => { + return pickBy(identity, { + created_by: signal.createdBy, + description: signal.alertTypeParams.description, + enabled: signal.enabled, + false_positives: signal.alertTypeParams.falsePositives, + filter: signal.alertTypeParams.filter, + filters: signal.alertTypeParams.filters, + from: signal.alertTypeParams.from, + id: signal.id, + immutable: signal.alertTypeParams.immutable, + index: signal.alertTypeParams.index, + interval: signal.interval, + rule_id: signal.alertTypeParams.ruleId, + language: signal.alertTypeParams.language, + max_signals: signal.alertTypeParams.maxSignals, + name: signal.name, + query: signal.alertTypeParams.query, + references: signal.alertTypeParams.references, + saved_id: signal.alertTypeParams.savedId, + severity: signal.alertTypeParams.severity, + size: signal.alertTypeParams.size, + updated_by: signal.updatedBy, + tags: signal.alertTypeParams.tags, + to: signal.alertTypeParams.to, + type: signal.alertTypeParams.type, + }); +}; + +export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { + if (isAlertTypes(findResults.data)) { + findResults.data = findResults.data.map(signal => transformAlertToSignal(signal)); + return findResults; + } else { + return new Boom('Internal error transforming', { statusCode: 500 }); + } +}; + +export const transformOrError = (signal: unknown): Partial | Boom => { + if (isAlertType(signal)) { + return transformAlertToSignal(signal); + } else { + return new Boom('Internal error transforming', { statusCode: 500 }); + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh index c393665315e25b..73882c78edfb83 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh @@ -9,8 +9,8 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal.sh ${id} +# Example: ./delete_signal_by_id.sh ${id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}/api/siem/signals/$1 | jq . + -X DELETE ${KIBANA_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh new file mode 100755 index 00000000000000..2b51146e6e1a0d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_signal_by_rule_id.sh ${rule_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh index f851bda0c12c91..473c7869361906 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh @@ -12,4 +12,4 @@ set -e # Example: ./find_signals.sh curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/siem/signals/_find | jq . + -X GET ${KIBANA_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh similarity index 76% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh index 7eb07e6e2dedf0..d10f347ff1f9e8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./read_signal.sh {id} +# Example: ./get_signal_by_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/siem/signals/$1 | jq . + -X GET ${KIBANA_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh new file mode 100755 index 00000000000000..302936fcb523e2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_signal_by_rule_id.sh {rule_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh index 6d79856ffd4fb7..837454dea71e6b 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh @@ -22,7 +22,7 @@ do { -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${KIBANA_URL}/api/siem/signals \ + -X POST ${KIBANA_URL}/api/detection_engine/rules \ -d @${SIGNAL} \ | jq .; } & diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh index 4736fbeda3cf47..326d47280c3061 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh @@ -20,9 +20,9 @@ do { -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${KIBANA_URL}/api/siem/signals \ + -X POST ${KIBANA_URL}/api/detection_engine/rules \ --data "{ - \"id\": \"${i}\", + \"rule_id\": \"${i}\", \"description\": \"Detecting root and admin users\", \"index\": [\"auditbeat-*\", \"filebeat-*\", \"packetbeat-*\", \"winlogbeat-*\"], \"interval\": \"24h\", @@ -31,7 +31,7 @@ do { \"type\": \"query\", \"from\": \"now-6m\", \"to\": \"now\", - \"query\": \"user.name: root or user.name: admin\" + \"query\": \"user.name: root or user.name: admin\", \"language\": \"kuery\" }" \ | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json index 0b6d222451303a..c6b2999b0e0c0c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json @@ -1,5 +1,5 @@ { - "id": "rule-1", + "rule_id": "rule-1", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json new file mode 100644 index 00000000000000..001b39bda5cbe4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json @@ -0,0 +1,13 @@ +{ + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "kuery", + "references": ["http://www.example.com", "https://ww.example.com"] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json index ad154e29045420..0b16d1ab03bd61 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json @@ -1,5 +1,5 @@ { - "id": "rule-2", + "rule_id": "rule-2", "description": "Detecting root and admin users over a long period of time", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json index be98c7757c1e22..f2d599b260f7b7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json @@ -1,5 +1,5 @@ { - "id": "rule-3", + "rule_id": "rule-3", "description": "Detecting root and admin users as an empty set", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json index 3c917af93fca8c..392877ec77df0c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json @@ -1,5 +1,5 @@ { - "id": "rule-4", + "rule_id": "rule-4", "description": "Detecting root and admin users with lucene", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json index 63728186b8f12c..f99f718f2adeea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json @@ -1,5 +1,5 @@ { - "id": "rule-5", + "rule_id": "rule-5", "description": "Detecting root and admin users over 24 hours on windows", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json index 58aefe12fb2d3f..c2d2eb1128deb5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json @@ -1,5 +1,5 @@ { - "id": "rule-6", + "rule_id": "rule-6", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json index ac0c41dc3d2154..99bb9b69c462af 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json @@ -1,5 +1,5 @@ { - "id": "rule-7", + "rule_id": "rule-7", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json index 66f308fc5e2fff..07ffb26ea0eca4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json @@ -1,5 +1,5 @@ { - "id": "rule-8", + "rule_id": "rule-8", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json index e7c348a98a7f26..06e66bbac4b605 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json @@ -1,5 +1,5 @@ { - "id": "rule-9", + "rule_id": "rule-9", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json index fc5f08234368de..cc4c3d2f7407f9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json @@ -1,5 +1,5 @@ { - "id": "rule-9999", + "rule_id": "rule-9999", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json index 9ab529ad4d9ce5..0dfb92b9098cbd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json @@ -1,5 +1,5 @@ { - "id": "rule-9999", + "rule_id": "rule-9999", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json index cd1a6efa73ad08..17dc207a62fa60 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json @@ -1,5 +1,5 @@ { - "id": "saved-query-1", + "rule_id": "saved-query-1", "description": "Detecting root and admin users", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json index 589583d417a133..5592ef7bdfd0c1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json @@ -1,7 +1,7 @@ { - "id": "rule-1", + "rule_id": "rule-1", "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], + "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json index 6b99e54d6a9b86..a15f671d6a0b19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json @@ -1,5 +1,5 @@ { - "id": "rule-1", + "rule_id": "rule-1", "description": "Changed Description of only detecting root user", "index": ["auditbeat-*"], "interval": "50m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json index 2f5457c352712c..d18ed01bd13d67 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json @@ -1,5 +1,5 @@ { - "id": "rule-longmont", + "rule_id": "rule-longmont", "description": "Detect Longmont activity", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh index 8cf69dc41e0be1..1d16aa6fc70624 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh @@ -10,13 +10,22 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNAL=${1:-./signals/root_or_admin_update_1.json} +SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) -# Example: ./update_signal.sh {id} ./signals/root_or_admin_1.json -curl -s -k \ - -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X PUT ${KIBANA_URL}/api/siem/signals \ - -d @${SIGNAL} \ - | jq . +# Example: ./update_signal.sh +# Example: ./update_signal.sh ./signals/root_or_admin_1.json +# Example glob: ./post_signal.sh ./signals/* +for SIGNAL in "${SIGNALS[@]}" +do { + [ -e "$SIGNAL" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}/api/detection_engine/rules \ + -d @${SIGNAL} \ + | jq .; +} & +done + +wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json index df4ea9bc3a0b2a..dd80e786a31214 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json @@ -23,6 +23,9 @@ } } }, + "id": { + "type": "keyword" + }, "original_time": { "type": "date" }, diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index ad099b728db36c..2769199ad1fb5a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -65,8 +65,9 @@ export interface SiemContext { export interface SignalHit { signal: { '@timestamp': string; + id: string; rule_revision: number; - rule_id: string; + rule_id: string | undefined | null; rule_type: string; parent: { id: string;