From 9b3581f93718a4f008e6cbf61ff10be4a59f2dc1 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 11 Feb 2020 22:23:34 -0700 Subject: [PATCH] [7.6] [SIEM][Detection Engine] Backend end-to-end tests (#57166) (#57392) * [SIEM][Detection Engine] Backend end-to-end tests * Adds end to end integration tests * Fixes a bug with import where on imports it was forcing all rules that were being imported to be set to be "enabled: false" instead of honoring what the original export has set for its enabled. * Adds a few "to be safe" await block so that the front end does not get a race condition within the bulk deletes and other parts of the code. * Fixes `statusCode` to be `status_code` and removes most of the Hapi Boomer errors * Changes PUT to be PATCH for partial updates * Adds true updates with PUT * Put some TODO blocks around existing bugs found in the API in the e2e tests that we might have time to get to or might not. This will let others maintaining the tests know that once they fix the bug they should update the end to end test to change the behavior. Testing this: Go to the latest CI logs and look for any particular lines from the test executing such as: ```ts should set the response content types to be expected ``` Also run this manually on your machine through this command: ```ts node scripts/functional_tests --config x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts ``` Change a test manually and re-run the above command to watch something fail. Screen shot of what you should see on the CI machine when these are running: Screen Shot 2020-02-08 at 10 15 21 AM Delete any items that are not applicable to this PR. ~~- [ ] 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)~~ ~~- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)~~ ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) * Removed key not used for end to end tests for 7.6 Co-authored-by: Elastic Machine --- .../containers/detection_engine/rules/api.ts | 2 +- .../plugins/siem/public/hooks/api/api.tsx | 2 + .../plugins/siem/public/utils/api/index.ts | 1 + .../plugins/siem/server/kibana.index.ts | 8 +- .../routes/__mocks__/request_responses.ts | 14 + .../routes/index/create_index_route.ts | 18 +- .../routes/index/delete_index_route.ts | 18 +- .../routes/index/read_index_route.ts | 16 +- .../privileges/read_privileges_route.ts | 10 +- .../rules/add_prepackaged_rules_route.test.ts | 3 +- .../rules/add_prepackaged_rules_route.ts | 18 +- .../routes/rules/create_rules_route.test.ts | 3 +- .../routes/rules/create_rules_route.ts | 39 +- .../routes/rules/delete_rules_bulk_route.ts | 84 +- .../routes/rules/delete_rules_route.ts | 30 +- .../routes/rules/export_rules_route.ts | 26 +- .../routes/rules/find_rules_route.ts | 22 +- ...et_prepackaged_rules_status_route.test.ts} | 0 .../get_prepackaged_rules_status_route.ts | 8 +- .../routes/rules/import_rules_route.ts | 26 +- .../routes/rules/patch_rules_bulk.test.ts | 160 +++ .../routes/rules/patch_rules_bulk_route.ts | 136 +++ .../routes/rules/patch_rules_route.test.ts | 129 +++ .../routes/rules/patch_rules_route.ts | 150 +++ .../routes/rules/read_rules_route.ts | 30 +- .../routes/rules/update_rules_bulk_route.ts | 10 +- .../routes/rules/update_rules_route.ts | 196 ++-- .../routes/rules/utils.test.ts | 86 +- .../detection_engine/routes/rules/utils.ts | 31 +- .../schemas/create_rules_bulk_schema.test.ts | 14 +- .../schemas/create_rules_schema.test.ts | 4 +- .../schemas/patch_rules_bulk_schema.test.ts | 52 + .../routes/schemas/patch_rules_bulk_schema.ts | 11 + .../routes/schemas/patch_rules_schema.test.ts | 1015 +++++++++++++++++ .../routes/schemas/patch_rules_schema.ts | 67 ++ .../schemas/query_rules_bulk_schema.test.ts | 18 +- .../routes/schemas/query_rules_schema.test.ts | 10 +- .../schemas/update_rules_bulk_schema.test.ts | 36 +- .../schemas/update_rules_schema.test.ts | 937 +++++++++------ .../routes/schemas/update_rules_schema.ts | 49 +- .../routes/tags/read_tags_route.ts | 8 +- .../lib/detection_engine/routes/utils.test.ts | 44 +- .../lib/detection_engine/routes/utils.ts | 27 +- .../detection_engine/rules/create_rules.ts | 3 +- .../lib/detection_engine/rules/patch_rules.ts | 151 +++ .../lib/detection_engine/rules/types.ts | 22 +- .../rules/update_prepacked_rules.ts | 4 +- .../detection_engine/rules/update_rules.ts | 142 +-- .../{update_rules.test.ts => utils.test.ts} | 8 +- .../lib/detection_engine/rules/utils.ts | 75 ++ .../detection_engine/scripts/patch_rule.sh | 31 + .../scripts/patch_rule_bulk.sh | 22 + .../rules/bulk/multiple_simplest_queries.json | 2 + .../{update_names.json => patch_names.json} | 0 .../scripts/rules/patches/README.md | 25 + .../scripts/rules/patches/disable_rule.json | 4 + .../scripts/rules/patches/enabled_rule.json | 4 + .../simplest_update_risk_score_by_id.json | 4 + ...simplest_update_risk_score_by_rule_id.json | 4 + .../rules/patches/simplest_updated_name.json | 4 + .../rules/patches/update_interval.json | 4 + .../patches/update_query_everything.json | 82 ++ .../scripts/rules/patches/update_tags.json | 4 + .../rules/patches/update_timelineid.json | 5 + .../scripts/rules/patches/update_version.json | 4 + .../scripts/rules/updates/README.md | 4 +- .../scripts/rules/updates/disable_rule.json | 6 + .../scripts/rules/updates/enabled_rule.json | 6 + .../simplest_update_risk_score_by_id.json | 9 +- ...simplest_update_risk_score_by_rule_id.json | 7 +- .../rules/updates/simplest_updated_name.json | 7 +- .../rules/updates/update_interval.json | 8 +- .../scripts/rules/updates/update_tags.json | 10 +- .../rules/updates/update_timelineid.json | 8 +- .../scripts/rules/updates/update_version.json | 8 +- .../detection_engine/scripts/update_rule.sh | 2 +- .../scripts/update_rule_bulk.sh | 2 +- x-pack/scripts/functional_tests.js | 1 + .../common/config.ts | 92 ++ .../common/ftr_provider_context.d.ts | 11 + .../common/services.ts | 7 + .../security_and_spaces/config.ts | 14 + .../tests/add_prepackaged_rules.ts | 92 ++ .../security_and_spaces/tests/create_rules.ts | 97 ++ .../tests/create_rules_bulk.ts | 124 ++ .../security_and_spaces/tests/delete_rules.ts | 123 ++ .../tests/delete_rules_bulk.ts | 293 +++++ .../security_and_spaces/tests/export_rules.ts | 134 +++ .../security_and_spaces/tests/find_rules.ts | 98 ++ .../tests/get_prepackaged_rules_status.ts | 100 ++ .../security_and_spaces/tests/import_rules.ts | 331 ++++++ .../security_and_spaces/tests/index.ts | 29 + .../security_and_spaces/tests/patch_rules.ts | 212 ++++ .../tests/patch_rules_bulk.ts | 356 ++++++ .../security_and_spaces/tests/read_rules.ts | 120 ++ .../security_and_spaces/tests/update_rules.ts | 221 ++++ .../tests/update_rules_bulk.ts | 386 +++++++ .../security_and_spaces/tests/utils.ts | 345 ++++++ 98 files changed, 6654 insertions(+), 781 deletions(-) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/{get_prepackaged_rule_status_route.test.ts => get_prepackaged_rules_status_route.test.ts} (100%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/{update_rules.test.ts => utils.test.ts} (93%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/{update_names.json => patch_names.json} (100%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json create mode 100644 x-pack/test/detection_engine_api_integration/common/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts create mode 100644 x-pack/test/detection_engine_api_integration/common/services.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8f4abeb31c226..5c9f86f503c2e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -149,7 +149,7 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => { if (body != null && body.message) { if (body.statusCode != null) { throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]); + } else if (body.status_code != null) { + throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.status_code}`]); } else { throw new ToasterErrors([body.message]); } diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/legacy/plugins/siem/public/utils/api/index.ts index 1dc14413b04d2..3c70083136505 100644 --- a/x-pack/legacy/plugins/siem/public/utils/api/index.ts +++ b/x-pack/legacy/plugins/siem/public/utils/api/index.ts @@ -8,6 +8,7 @@ export interface MessageBody { error?: string; message?: string; statusCode?: number; + status_code?: number; } export const parseJsonFromBody = async (response: Response): Promise => { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index a488db3f0c3d7..bab7936005c04 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -13,7 +13,7 @@ import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_r import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route'; import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route'; import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; -import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; +import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route'; import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; import { ServerFacade } from './types'; @@ -23,12 +23,14 @@ import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_rout import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route'; import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; -import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route'; import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route'; import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route'; import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route'; import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; +import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; const APP_ID = 'siem'; @@ -50,12 +52,14 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy updateRulesRoute(__legacy); deleteRulesRoute(__legacy); findRulesRoute(__legacy); + patchRulesRoute(__legacy); addPrepackedRulesRoute(__legacy); getPrepackagedRulesStatusRoute(__legacy); createRulesBulkRoute(__legacy); updateRulesBulkRoute(__legacy); deleteRulesBulkRoute(__legacy); + patchRulesBulkRoute(__legacy); importRulesRoute(__legacy); exportRulesRoute(__legacy); 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 65e1a79b67bf4..85570b1b59656 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 @@ -108,6 +108,14 @@ export const getUpdateRequest = (): ServerInjectOptions => ({ }, }); +export const getPatchRequest = (): ServerInjectOptions => ({ + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...typicalPayload(), + }, +}); + export const getReadRequest = (): ServerInjectOptions => ({ method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`, @@ -130,6 +138,12 @@ export const getUpdateBulkRequest = (): ServerInjectOptions => ({ payload: [typicalPayload()], }); +export const getPatchBulkRequest = (): ServerInjectOptions => ({ + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], +}); + export const getDeleteBulkRequest = (): ServerInjectOptions => ({ method: 'DELETE', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index 0eb090179b192..e0d48836013ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import Boom from 'boom'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import signalsPolicy from './signals_policy.json'; @@ -31,13 +30,18 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade) { + async handler(request: RequestFacade, headers) { try { const index = getIndex(request, server); const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, index); if (indexExists) { - return new Boom(`index: "${index}" already exists`, { statusCode: 409 }); + return headers + .response({ + message: `index: "${index}" already exists`, + status_code: 409, + }) + .code(409); } else { const policyExists = await getPolicyExists(callWithRequest, index); if (!policyExists) { @@ -52,7 +56,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = return { acknowledged: true }; } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index 82fe0f55215fb..c1edc824b81eb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import Boom from 'boom'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../../types'; @@ -39,13 +38,18 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade) { + async handler(request: RequestFacade, headers) { try { const index = getIndex(request, server); const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, index); if (!indexExists) { - return new Boom(`index: "${index}" does not exist`, { statusCode: 404 }); + return headers + .response({ + message: `index: "${index}" does not exist`, + status_code: 404, + }) + .code(404); } else { await deleteAllIndex(callWithRequest, `${index}-*`); const policyExists = await getPolicyExists(callWithRequest, index); @@ -59,7 +63,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = return { acknowledged: true }; } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index a8c4b7407c448..1a5018d446747 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -5,7 +5,6 @@ */ import Hapi from 'hapi'; -import Boom from 'boom'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../../types'; @@ -42,11 +41,22 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => if (request.method.toLowerCase() === 'head') { return headers.response().code(404); } else { - return new Boom('index for this space does not exist', { statusCode: 404 }); + return headers + .response({ + message: 'index for this space does not exist', + status_code: 404, + }) + .code(404); } } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 5ea4dc7595b2b..45ecb7dc97288 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -24,7 +24,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }, }, }, - async handler(request: RulesRequest) { + async handler(request: RulesRequest, headers) { try { const callWithRequest = callWithRequestFactory(request, server); const index = getIndex(request, server); @@ -35,7 +35,13 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve has_encryption_key: !usingEphemeralEncryptionKey, }); } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 1014fbbfbcdaa..efc5d0ead6d90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -105,10 +105,9 @@ describe('add_prepackaged_rules_route', () => { alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ - error: 'Bad Request', message: 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', - statusCode: 400, + status_code: 400, }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 3e2337f2bb5a5..81f1d2a99d680 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -6,7 +6,6 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import Boom from 'boom'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../../types'; @@ -55,9 +54,12 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex); if (!spaceIndexExists) { - return Boom.badRequest( - `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}` - ); + return headers + .response({ + message: `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`, + status_code: 400, + }) + .code(400); } } await Promise.all( @@ -75,7 +77,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR rules_updated: rulesToUpdate.length, }; } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 77c6f6f3b4840..98b1f9415ad4d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -91,9 +91,8 @@ describe('create_rules', () => { alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getCreateRequest()); expect(JSON.parse(payload)).toEqual({ - error: 'Bad Request', message: 'To create a rule, the index must exist first. Index .siem-signals does not exist', - statusCode: 400, + status_code: 400, }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 5953624925bbb..8f9ed88e62309 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -6,7 +6,6 @@ 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 { createRules } from '../../rules/create_rules'; @@ -15,7 +14,7 @@ import { createRulesSchema } from '../schemas/create_rules_schema'; import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { transformOrError } from './utils'; +import { transform } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; import { callWithRequestFactory, getIndex, transformError } from '../utils'; @@ -75,14 +74,22 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, finalIndex); if (!indexExists) { - return Boom.badRequest( - `To create a rule, the index must exist first. Index ${finalIndex} does not exist` - ); + return headers + .response({ + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + status_code: 400, + }) + .code(400); } if (ruleId != null) { const rule = await readRules({ alertsClient, ruleId }); if (rule != null) { - return Boom.conflict(`rule_id: "${ruleId}" already exists`); + return headers + .response({ + message: `rule_id: "${ruleId}" already exists`, + status_code: 409, + }) + .code(409); } } const createdRule = await createRules({ @@ -125,9 +132,25 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = search: `${createdRule.id}`, searchFields: ['alertId'], }); - return transformOrError(createdRule, ruleStatuses.saved_objects[0]); + const transformed = transform(createdRule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index b77ae56a486c8..bc62ac6d9df94 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -27,49 +27,51 @@ export const createDeleteRulesBulkRoute: Hapi.ServerRoute = { }, payload: queryRulesBulkSchema, }, - }, - async handler(request: QueryBulkRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !actionsClient || !savedObjectsClient) { - return headers.response().code(404); - } - const rules = Promise.all( - request.payload.map(async payloadRule => { - const { id, rule_id: ruleId } = payloadRule; - const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; - try { - const rule = await deleteRules({ - actionsClient, - alertsClient, - id, - ruleId, - }); - if (rule != null) { - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 6, - search: rule.id, - searchFields: ['alertId'], + async handler(request: QueryBulkRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + const rules = await Promise.all( + request.payload.map(async payloadRule => { + const { id, rule_id: ruleId } = payloadRule; + const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + try { + const rule = await deleteRules({ + actionsClient, + alertsClient, + id, + ruleId, }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); - return transformOrBulkError(idOrRuleIdOrUnknown, rule); - } else { - return getIdBulkError({ id, ruleId }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 6, + search: rule.id, + searchFields: ['alertId'], + }); + ruleStatuses.saved_objects.forEach(async obj => + savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) + ); + return transformOrBulkError(idOrRuleIdOrUnknown, rule); + } else { + return getIdBulkError({ id, ruleId }); + } + } catch (err) { + return transformBulkError(idOrRuleIdOrUnknown, err); } - } catch (err) { - return transformBulkError(idOrRuleIdOrUnknown, err); - } - }) - ); - return rules; + }) + ); + return rules; + }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index ea8b54a79a5e3..f1ae7fe80056e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -11,7 +11,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; import { ServerFacade } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -58,12 +58,34 @@ export const createDeleteRulesRoute: Hapi.ServerRoute = { ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - return transformOrError(rule, ruleStatuses.saved_objects[0]); + const transformed = transform(rule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } } else { - return getIdError({ id, ruleId }); + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index f9918301c6b3c..badcd25f15d33 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -14,6 +13,7 @@ import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_r import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; +import { transformError } from '../utils'; export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -42,11 +42,21 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = try { const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + return headers + .response({ + message: `Can't export more than ${exportSizeLimit} rules`, + status_code: 400, + }) + .code(400); } else { const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); if (nonPackagedRulesCount > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + return headers + .response({ + message: `Can't export more than ${exportSizeLimit} rules`, + status_code: 400, + }) + .code(400); } } @@ -62,8 +72,14 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = return response .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) .header('Content-Type', 'application/ndjson'); - } catch { - return Boom.badRequest(`Sorry, something went wrong to export rules`); + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 6cbc794751f9f..949453dedbe39 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -11,7 +11,7 @@ import { findRules } from '../../rules/find_rules'; import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; import { ServerFacade } from '../../../../types'; -import { transformFindAlertsOrError } from './utils'; +import { transformFindAlerts } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -60,9 +60,25 @@ export const createFindRulesRoute: Hapi.ServerRoute = { return results; }) ); - return transformFindAlertsOrError(rules, ruleStatuses); + const transformed = transformFindAlerts(rules, ruleStatuses); + if (transformed == null) { + return headers + .response({ + message: 'unknown data type, error transforming alert', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index faf0282296632..b3f13e48bf138 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -58,7 +58,13 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { rules_not_updated: rulesToUpdate.length, }; } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index c6096bea352a6..5843e290bb899 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import Hapi from 'hapi'; import { chunk, isEmpty, isFunction } from 'lodash/fp'; import { extname } from 'path'; @@ -24,17 +23,11 @@ import { } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; -import { updateRules } from '../../rules/update_rules'; +import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; type PromiseFromStreams = ImportRuleAlertRest | Error; -/* - * We were getting some error like that possible EventEmitter memory leak detected - * So we decide to batch the update by 10 to avoid any complication in the node side - * https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n - * - */ const CHUNK_PARSED_OBJECT_SIZE = 10; export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { @@ -70,13 +63,17 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const { filename } = request.payload.file.hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { - return Boom.badRequest(`Invalid file extension ${fileExtension}`); + return headers + .response({ + message: `Invalid file extension ${fileExtension}`, + status_code: 400, + }) + .code(400); } const objectLimit = server.config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); const parsedObjects = await createPromiseFromStreams([readStream]); - const uniqueParsedObjects = Array.from( parsedObjects .reduce( @@ -121,6 +118,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = } const { description, + enabled, false_positives: falsePositives, from, immutable, @@ -165,7 +163,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = alertsClient, actionsClient, description, - enabled: false, + enabled, falsePositives, from, immutable, @@ -193,12 +191,12 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { - await updateRules({ + await patchRules({ alertsClient, actionsClient, savedObjectsClient, description, - enabled: false, + enabled, falsePositives, from, immutable, @@ -231,7 +229,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = createBulkErrorObject({ ruleId, statusCode: 409, - message: `This Rule "${rule.name}" already exists`, + message: `rule_id: "${ruleId}" already exists`, }) ); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts new file mode 100644 index 0000000000000..aa0dd04786a2e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { + createMockServer, + createMockServerWithoutAlertClientDecoration, +} from '../__mocks__/_mock_server'; + +import { patchRulesRoute } from './patch_rules_route'; +import { ServerInjectOptions } from 'hapi'; + +import { + getFindResult, + getResult, + updateActionResult, + typicalPayload, + getFindResultWithSingleHit, + getPatchBulkRequest, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { patchRulesBulkRoute } from './patch_rules_bulk_route'; +import { BulkError } from '../utils'; + +describe('patch_rules_bulk', () => { + let { server, alertsClient, actionsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient } = createMockServer()); + patchRulesBulkRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getPatchBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 200 as a response when updating a single rule 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(getPatchBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 within the payload when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { payload } = await server.inject(getPatchBulkRequest()); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + patchRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if id is not given in either the body or the url', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + const { rule_id, ...noId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [noId], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns errors as 200 to just indicate ok something happened', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toEqual(200); + }); + + test('returns 404 in the payload if the record does not exist yet', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const parsed: BulkError[] = JSON.parse(payload); + const expected: BulkError[] = [ + { + error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, + rule_id: 'rule-1', + }, + ]; + expect(parsed).toEqual(expected); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [ + { + ...noType, + type: 'something-made-up', + }, + ], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts new file mode 100644 index 0000000000000..846899877afb4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -0,0 +1,136 @@ +/* + * 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 Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { + BulkPatchRulesRequest, + IRuleSavedAttributesSavedObjectAttributes, +} from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { transformOrBulkError, getIdBulkError } from './utils'; +import { transformBulkError } from '../utils'; +import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; +import { patchRules } from '../../rules/patch_rules'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; + +export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PATCH', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: patchRulesBulkSchema, + }, + }, + async handler(request: BulkPatchRulesRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + + const rules = await Promise.all( + request.payload.map(async payloadRule => { + const { + description, + enabled, + false_positives: falsePositives, + from, + query, + language, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId, + timeline_title: timelineTitle, + meta, + filters, + rule_id: ruleId, + id, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + } = payloadRule; + const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + try { + const rule = await patchRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + query, + language, + outputIndex, + savedId, + savedObjectsClient, + timelineId, + timelineTitle, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); + } else { + return getIdBulkError({ id, ruleId }); + } + } catch (err) { + return transformBulkError(idOrRuleIdOrUnknown, err); + } + }) + ); + return rules; + }, + }; +}; + +export const patchRulesBulkRoute = (server: ServerFacade): void => { + server.route(createPatchRulesBulkRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts new file mode 100644 index 0000000000000..d315d45046e2d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -0,0 +1,129 @@ +/* + * 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 { + createMockServer, + createMockServerWithoutAlertClientDecoration, +} from '../__mocks__/_mock_server'; + +import { patchRulesRoute } from './patch_rules_route'; +import { ServerInjectOptions } from 'hapi'; + +import { + getFindResult, + getFindResultStatus, + getResult, + updateActionResult, + getPatchRequest, + typicalPayload, + getFindResultWithSingleHit, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; + +describe('patch_rules', () => { + let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); + patchRulesRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { statusCode } = await server.inject(getPatchRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { statusCode } = await server.inject(getPatchRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + patchRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if id is not given in either the body or the url', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { rule_id, ...noId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: { + payload: noId, + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns 404 if the record does not exist yet', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'PATCH', + 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(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: typicalPayload(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PATCH', + url: DETECTION_ENGINE_RULES_URL, + payload: { + ...noType, + type: 'something-made-up', + }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts new file mode 100644 index 0000000000000..054a54c6f33c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -0,0 +1,150 @@ +/* + * 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 Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { patchRules } from '../../rules/patch_rules'; +import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; +import { patchRulesSchema } from '../schemas/patch_rules_schema'; +import { ServerFacade } from '../../../../types'; +import { getIdError, transform } from './utils'; +import { transformError } from '../utils'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; + +export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PATCH', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: patchRulesSchema, + }, + }, + async handler(request: PatchRulesRequest, headers) { + const { + description, + enabled, + false_positives: falsePositives, + from, + query, + language, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId, + timeline_title: timelineTitle, + meta, + filters, + rule_id: ruleId, + id, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + } = request.payload; + + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + + try { + const rule = await patchRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + query, + language, + outputIndex, + savedId, + savedObjectsClient, + timelineId, + timelineTitle, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + const transformed = transform(rule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } + } else { + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } + }, + }; +}; + +export const patchRulesRoute = (server: ServerFacade) => { + server.route(createPatchRulesRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 25d8e6770f699..be32852b26ac1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -7,7 +7,7 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; @@ -55,12 +55,34 @@ export const createReadRulesRoute: Hapi.ServerRoute = { search: rule.id, searchFields: ['alertId'], }); - return transformOrError(rule, ruleStatuses.saved_objects[0]); + const transformedOrError = transform(rule, ruleStatuses.saved_objects[0]); + if (transformedOrError == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformedOrError; + } } else { - return getIdError({ id, ruleId }); + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 5f75696a368d3..2d86fede99609 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -13,10 +13,10 @@ import { } from '../../rules/types'; import { ServerFacade } from '../../../../types'; import { transformOrBulkError, getIdBulkError } from './utils'; -import { transformBulkError } from '../utils'; +import { transformBulkError, getIndex } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; -import { updateRules } from '../../rules/update_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { updateRules } from '../../rules/update_rules'; export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -43,7 +43,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou return headers.response().code(404); } - const rules = Promise.all( + const rules = await Promise.all( request.payload.map(async payloadRule => { const { description, @@ -73,6 +73,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou references, version, } = payloadRule; + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { const rule = await updateRules({ @@ -80,11 +81,12 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou actionsClient, description, enabled, + immutable: false, falsePositives, from, query, language, - outputIndex, + outputIndex: finalIndex, savedId, savedObjectsClient, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a36ae632de15b..fbb77e4d936e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -7,88 +7,47 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { updateRules } from '../../rules/update_rules'; import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; import { ServerFacade } from '../../../../types'; -import { getIdError, transformOrError } from './utils'; -import { transformError } from '../utils'; +import { getIdError, transform } from './utils'; +import { transformError, getIndex } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesRoute: Hapi.ServerRoute = { - method: 'PUT', - path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:siem'], - validate: { - options: { - abortEarly: false, +export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PUT', + path: DETECTION_ENGINE_RULES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: updateRulesSchema, }, - payload: updateRulesSchema, }, - }, - async handler(request: UpdateRulesRequest, headers) { - const { - description, - enabled, - false_positives: falsePositives, - from, - query, - language, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId = null, - timeline_title: timelineTitle = null, - meta, - filters, - rule_id: ruleId, - id, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - } = request.payload; - - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !actionsClient || !savedObjectsClient) { - return headers.response().code(404); - } - - try { - const rule = await updateRules({ - alertsClient, - actionsClient, + async handler(request: UpdateRulesRequest, headers) { + const { description, enabled, - falsePositives, + false_positives: falsePositives, from, query, language, - outputIndex, - savedId, - savedObjectsClient, - timelineId, - timelineTitle, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId = null, + timeline_title: timelineTitle = null, meta, filters, + rule_id: ruleId, id, - ruleId, index, interval, - maxSignals, - riskScore, + max_signals: maxSignals, + risk_score: riskScore, name, severity, tags, @@ -97,28 +56,97 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { threat, references, version, - }); - if (rule != null) { - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], + } = request.payload; + + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { + return headers.response().code(404); + } + + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const rule = await updateRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable: false, + query, + language, + outputIndex: finalIndex, + savedId, + savedObjectsClient, + timelineId, + timelineTitle, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, }); - return transformOrError(rule, ruleStatuses.saved_objects[0]); - } else { - return getIdError({ id, ruleId }); + if (rule != null) { + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + const transformed = transform(rule, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return headers + .response({ + message: 'Internal error transforming rules', + status_code: 500, + }) + .code(500); + } else { + return transformed; + } + } else { + const error = getIdError({ id, ruleId }); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); + } + } catch (err) { + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } - } catch (err) { - return transformError(err); - } - }, + }, + }; }; export const updateRulesRoute = (server: ServerFacade) => { - server.route(createUpdateRulesRoute); + server.route(createUpdateRulesRoute(server)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index ec11a8fb2da39..7e7d67333e78d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - import { transformAlertToRule, getIdError, - transformFindAlertsOrError, - transformOrError, + transformFindAlerts, + transform, transformTags, getIdBulkError, transformOrBulkError, @@ -547,55 +545,87 @@ describe('utils', () => { }); describe('getIdError', () => { + test('it should have a status code', () => { + const error = getIdError({ id: '123', ruleId: undefined }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); + }); + 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: "123" not found'); + const error = getIdError({ id: '123', ruleId: undefined }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); }); 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: "123" not found'); + const error = getIdError({ id: '123', ruleId: null }); + expect(error).toEqual({ + message: 'id: "123" not found', + statusCode: 404, + }); }); 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: "rule-id-123" not found'); + const error = getIdError({ id: undefined, ruleId: 'rule-id-123' }); + expect(error).toEqual({ + message: 'rule_id: "rule-id-123" not found', + statusCode: 404, + }); }); 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: "rule-id-123" not found'); + const error = getIdError({ id: null, ruleId: 'rule-id-123' }); + expect(error).toEqual({ + message: 'rule_id: "rule-id-123" not found', + statusCode: 404, + }); }); 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'); + const error = getIdError({ id: undefined, ruleId: undefined }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); 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'); + const error = getIdError({ id: null, ruleId: null }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); 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'); + const error = getIdError({ id: null, ruleId: undefined }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); 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'); + const error = getIdError({ id: undefined, ruleId: null }); + expect(error).toEqual({ + message: 'id or rule_id should have been defined', + statusCode: 404, + }); }); }); - describe('transformFindAlertsOrError', () => { + describe('transformFindAlerts', () => { test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlertsOrError({ data: [] }); + const output = transformFindAlerts({ data: [] }); expect(output).toEqual({ data: [] }); }); test('outputs 200 if the data is of type siem alert', () => { - const output = transformFindAlertsOrError({ + const output = transformFindAlerts({ data: [getResult()], }); const expected: OutputRuleAlertRest = { @@ -663,14 +693,14 @@ describe('utils', () => { }); 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'); + const output = transformFindAlerts({ data: [{ random: 1 }] }); + expect(output).toBeNull(); }); }); describe('transformOrError', () => { test('outputs 200 if the data is of type siem alert', () => { - const output = transformOrError(getResult()); + const output = transform(getResult()); const expected: OutputRuleAlertRest = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', @@ -734,8 +764,8 @@ describe('utils', () => { }); 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'); + const output = transform({ data: [{ random: 1 }] }); + expect(output).toBeNull(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index b45db53c13d88..abb94c10209dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { pickBy } from 'lodash/fp'; import { SavedObject } from 'kibana/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; @@ -24,6 +23,7 @@ import { createSuccessObject, ImportSuccessError, createImportErrorObject, + OutputError, } from '../utils'; export const getIdError = ({ @@ -32,13 +32,22 @@ export const getIdError = ({ }: { id: string | undefined | null; ruleId: string | undefined | null; -}) => { +}): OutputError => { if (id != null) { - return Boom.notFound(`id: "${id}" not found`); + return { + message: `id: "${id}" not found`, + statusCode: 404, + }; } else if (ruleId != null) { - return Boom.notFound(`rule_id: "${ruleId}" not found`); + return { + message: `rule_id: "${ruleId}" not found`, + statusCode: 404, + }; } else { - return Boom.notFound('id or rule_id should have been defined'); + return { + message: 'id or rule_id should have been defined', + statusCode: 404, + }; } }; @@ -136,10 +145,10 @@ export const transformAlertsToRules = ( return alerts.map(alert => transformAlertToRule(alert)); }; -export const transformFindAlertsOrError = ( +export const transformFindAlerts = ( findResults: { data: unknown[] }, ruleStatuses?: unknown[] -): unknown | Boom => { +): unknown | null => { if (!ruleStatuses && isAlertTypes(findResults.data)) { findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; @@ -150,14 +159,14 @@ export const transformFindAlertsOrError = ( ); return findResults; } else { - return new Boom('Internal error transforming', { statusCode: 500 }); + return null; } }; -export const transformOrError = ( +export const transform = ( alert: unknown, ruleStatus?: unknown -): Partial | Boom => { +): Partial | null => { if (!ruleStatus && isAlertType(alert)) { return transformAlertToRule(alert); } @@ -166,7 +175,7 @@ export const transformOrError = ( } else if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { return transformAlertToRule(alert, ruleStatus); } else { - return new Boom('Internal error transforming', { statusCode: 500 }); + return null; } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts index 1eab50848b822..2a64478962ced 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts @@ -5,7 +5,7 @@ */ import { createRulesBulkSchema } from './create_rules_bulk_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; // only the basics of testing are here. // see: create_rules_schema.test.ts for the bulk of the validation tests @@ -13,7 +13,7 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types'; describe('create_rules_bulk_schema', () => { test('can take an empty array and validate it', () => { expect( - createRulesBulkSchema.validate>>([]).error + createRulesBulkSchema.validate>>([]).error ).toBeFalsy(); }); @@ -29,7 +29,7 @@ describe('create_rules_bulk_schema', () => { test('single array of [id] does validate', () => { expect( - createRulesBulkSchema.validate>>([ + createRulesBulkSchema.validate>>([ { rule_id: 'rule-1', risk_score: 50, @@ -49,7 +49,7 @@ describe('create_rules_bulk_schema', () => { test('two values of [id] does validate', () => { expect( - createRulesBulkSchema.validate>>([ + createRulesBulkSchema.validate>>([ { rule_id: 'rule-1', risk_score: 50, @@ -82,7 +82,7 @@ describe('create_rules_bulk_schema', () => { test('The default for "from" will be "now-6m"', () => { expect( - createRulesBulkSchema.validate>([ + createRulesBulkSchema.validate>([ { rule_id: 'rule-1', risk_score: 50, @@ -102,7 +102,7 @@ describe('create_rules_bulk_schema', () => { test('The default for "to" will be "now"', () => { expect( - createRulesBulkSchema.validate>([ + createRulesBulkSchema.validate>([ { rule_id: 'rule-1', risk_score: 50, @@ -122,7 +122,7 @@ describe('create_rules_bulk_schema', () => { test('You cannot set the severity to a value other than low, medium, high, or critical', () => { expect( - createRulesBulkSchema.validate>([ + createRulesBulkSchema.validate>([ { rule_id: 'rule-1', risk_score: 50, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index d9605a265d28b..052a149f3d4dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -5,12 +5,12 @@ */ import { createRulesSchema } from './create_rules_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams, RuleAlertParamsRest } from '../../types'; describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createRulesSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts new file mode 100644 index 0000000000000..cbcb9eba75bc1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { patchRulesBulkSchema } from './patch_rules_bulk_schema'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; + +// only the basics of testing are here. +// see: patch_rules_schema.test.ts for the bulk of the validation tests +// this just wraps patchRulesSchema in an array +describe('patch_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + expect( + patchRulesBulkSchema.validate>>([]).error + ).toBeFalsy(); + }); + + test('made up values do not validate', () => { + expect( + patchRulesBulkSchema.validate<[{ madeUp: string }]>([ + { + madeUp: 'hi', + }, + ]).error + ).toBeTruthy(); + }); + + test('single array of [id] does validate', () => { + expect( + patchRulesBulkSchema.validate>>([ + { + id: 'rule-1', + }, + ]).error + ).toBeFalsy(); + }); + + test('two values of [id] does validate', () => { + expect( + patchRulesBulkSchema.validate>>([ + { + id: 'rule-1', + }, + { + id: 'rule-2', + }, + ]).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts new file mode 100644 index 0000000000000..ff813bce84add --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts @@ -0,0 +1,11 @@ +/* + * 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 Joi from 'joi'; + +import { patchRulesSchema } from './patch_rules_schema'; + +export const patchRulesBulkSchema = Joi.array().items(patchRulesSchema); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts new file mode 100644 index 0000000000000..11bed22e1c047 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -0,0 +1,1015 @@ +/* + * 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 { patchRulesSchema } from './patch_rules_schema'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { ThreatParams } from '../../types'; + +describe('patch rules schema', () => { + test('empty objects do not validate as they require at least id or rule_id', () => { + expect(patchRulesSchema.validate>({}).error).toBeTruthy(); + }); + + test('made up values do not validate', () => { + expect( + patchRulesSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[id] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[rule_id] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[id] and [rule_id] does not validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'id-1', + rule_id: 'rule-1', + }).error.message + ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); + }); + + test('[rule_id, description] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, description] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, risk_score] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + risk_score: 10, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name] does validate', () => { + expect( + patchRulesSchema.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( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + 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( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('allows references to be sent as a valid value to patch with', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('does not default references to an array', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual(undefined); + }); + + test('does not default interval', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + type: 'query', + }).value.interval + ).toEqual(undefined); + }); + + test('does not default max signal', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(undefined); + }); + + test('references cannot be numbers', () => { + expect( + patchRulesSchema.validate< + Partial> & { references: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); + }); + + test('indexes cannot be numbers', () => { + expect( + patchRulesSchema.validate< + Partial> & { index: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); + }); + + test('saved_id is not required when type is saved_query and will validate without it', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + }).error + ).toBeFalsy(); + }); + + test('saved_id validates with saved_query', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type can have filters with it', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeFalsy(); + }); + + test('language validates with kuery', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error.message + ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); + }); + + test('max_signals cannot be negative', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); + }); + + test('max_signals cannot be zero', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); + }); + + test('max_signals can be 1', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('meta can be patched', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + meta: { whateverYouWant: 'anything_at_all' }, + }).error + ).toBeFalsy(); + }); + + test('You cannot patch meta as a string', () => { + expect( + patchRulesSchema.validate & { meta: string }>>( + { + id: 'rule-1', + meta: 'should not work', + } + ).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); + }); + + test('filters cannot be a string', () => { + expect( + patchRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + type: 'query', + filters: 'some string', + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); + }); + + test('threat is not defaulted to empty array on patch', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).value.threat + ).toBe(undefined); + }); + + test('threat is not defaulted to undefined on patch with empty array', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [], + }).value.threat + ).toMatchObject([]); + }); + + test('threat is valid when updated with all sub-objects', () => { + const expected: ThreatParams[] = [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ]; + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).value.threat + ).toMatchObject(expected); + }); + + test('threat is invalid when updated with missing property framework', () => { + expect( + patchRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); + }); + + test('threat is invalid when updated with missing tactic sub-object', () => { + expect( + patchRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + ); + }); + + test('threat is invalid when updated with missing technique', () => { + expect( + patchRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threat: [ + { + framework: 'fake', + tactic: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' + ); + }); + + test('validates with timeline_id and timeline_title', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + timeline_title: 'some-title', + }).error + ).toBeFalsy(); + }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'timeline-id', + timeline_title: null, + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + timeline_title: '', + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: '', + timeline_title: 'some-title', + }).error.message + ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_title: 'some-title', + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); + }); + + test('You cannot set the severity to a value other than low, medium, high, or critical', () => { + expect( + patchRulesSchema.validate>({ + id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'junk', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).error.message + ).toEqual( + 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts new file mode 100644 index 0000000000000..d0ed1af01833b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -0,0 +1,67 @@ +/* + * 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 Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + enabled, + description, + false_positives, + filters, + from, + index, + rule_id, + interval, + query, + language, + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threat, + references, + id, + version, +} from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const patchRulesSchema = Joi.object({ + description, + enabled, + false_positives, + filters, + from, + rule_id, + id, + index, + interval, + query: query.allow(''), + language, + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threat, + references, + version, +}).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts index ab1ffaab49165..7ea7fcbd1d86b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts @@ -5,7 +5,7 @@ */ import { queryRulesBulkSchema } from './query_rules_bulk_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; // only the basics of testing are here. // see: query_rules_bulk_schema.test.ts for the bulk of the validation tests @@ -13,13 +13,13 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types'; describe('query_rules_bulk_schema', () => { test('can take an empty array and validate it', () => { expect( - queryRulesBulkSchema.validate>>([]).error + queryRulesBulkSchema.validate>>([]).error ).toBeFalsy(); }); test('both rule_id and id being supplied do not validate', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { rule_id: '1', id: '1', @@ -32,7 +32,7 @@ describe('query_rules_bulk_schema', () => { test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { id: '1', }, @@ -48,13 +48,13 @@ describe('query_rules_bulk_schema', () => { test('only id validates', () => { expect( - queryRulesBulkSchema.validate>>([{ id: '1' }]).error + queryRulesBulkSchema.validate>>([{ id: '1' }]).error ).toBeFalsy(); }); test('only id validates with two elements', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { id: '1' }, { id: '2' }, ]).error @@ -63,14 +63,14 @@ describe('query_rules_bulk_schema', () => { test('only rule_id validates', () => { expect( - queryRulesBulkSchema.validate>>([{ rule_id: '1' }]) + queryRulesBulkSchema.validate>>([{ rule_id: '1' }]) .error ).toBeFalsy(); }); test('only rule_id validates with two elements', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { rule_id: '1' }, { rule_id: '2' }, ]).error @@ -79,7 +79,7 @@ describe('query_rules_bulk_schema', () => { test('both id and rule_id validates with two separate elements', () => { expect( - queryRulesBulkSchema.validate>>([ + queryRulesBulkSchema.validate>>([ { id: '1' }, { rule_id: '2' }, ]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts index c89d60e773a77..0f392e399f36c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts @@ -5,29 +5,29 @@ */ import { queryRulesSchema } from './query_rules_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; describe('queryRulesSchema', () => { test('empty objects do not validate', () => { - expect(queryRulesSchema.validate>({}).error).toBeTruthy(); + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); test('both rule_id and id being supplied do not validate', () => { expect( - queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error .message ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); }); test('only id validates', () => { expect( - queryRulesSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - queryRulesSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts index 2b1bad39eb686..e866260662ad7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts @@ -31,7 +31,17 @@ describe('update_rules_bulk_schema', () => { expect( updateRulesBulkSchema.validate>>([ { - id: 'rule-1', + id: 'id-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }, ]).error ).toBeFalsy(); @@ -41,10 +51,30 @@ describe('update_rules_bulk_schema', () => { expect( updateRulesBulkSchema.validate>>([ { - id: 'rule-1', + id: 'id-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }, { - id: 'rule-2', + id: 'id-2', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }, ]).error ).toBeFalsy(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index 0dc9f3df3da1c..c7899f3afa7b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -5,169 +5,107 @@ */ import { updateRulesSchema } from './update_rules_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams } from '../../types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { ThreatParams, RuleAlertParamsRest } from '../../types'; -describe('update rules schema', () => { +describe('create rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect(updateRulesSchema.validate>({}).error).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); }); - test('[id] does validate', () => { + test('[rule_id] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[rule_id] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[id and rule_id] does not validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'id-1', + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); }); - test('[rule_id, description] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, description] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, risk_score] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - risk_score: 10, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to] does validate', () => { + test('[id] and [rule_id] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ + id: 'id-1', rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', - }).error - ).toBeFalsy(); + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }).error.message + ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); }); - test('[id, description, from, to] does validate', () => { + test('[rule_id, description] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', - from: 'now-5m', - to: 'now', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name] does validate', () => { + test('[rule_id, description, from] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', - to: 'now', - name: 'some-name', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name] does validate', () => { + test('[rule_id, description, from, to] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', - name: 'some-name', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name, severity] does validate', () => { + test('[rule_id, description, from, to, name] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', - severity: 'low', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name, severity] does validate', () => { + test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name, severity, type] does validate', () => { + test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -176,56 +114,61 @@ describe('update rules schema', () => { severity: 'low', type: 'query', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type] does validate', () => { + test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', + interval: '5m', type: 'query', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', - interval: '5m', type: 'query', + interval: '5m', + index: ['index-1'], }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, name, severity, type, interval] does validate', () => { + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', name: 'some-name', severity: 'low', - interval: '5m', type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -235,14 +178,17 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + query: 'some query', + language: 'kuery', }).error - ).toBeFalsy(); + ).toBeTruthy(); }); - test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -251,14 +197,18 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + query: 'some query', + language: 'kuery', }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -268,14 +218,15 @@ describe('update rules schema', () => { interval: '5m', type: 'query', query: 'some query', + language: 'kuery', }).error ).toBeFalsy(); }); - test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', description: 'some description', from: 'now-5m', to: 'now', @@ -284,15 +235,17 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - query: 'some query', + risk_score: 50, }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -301,16 +254,16 @@ describe('update rules schema', () => { severity: 'low', 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', () => { + test('You can send in an empty array to threat', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -319,16 +272,21 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + references: ['index-1'], query: 'some query', language: 'kuery', + max_signals: 1, + threat: [], }).error ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { expect( - updateRulesSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -337,14 +295,33 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + threat: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], }).error ).toBeFalsy(); }); - test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { + test('allows references to be sent as valid', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -353,14 +330,19 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', }).error ).toBeFalsy(); }); - test('allows references to be sent as a valid value to update with', () => { + test('defaults references to an array', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -369,17 +351,20 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - references: ['index-1'], - query: 'some query', + query: 'some-query', language: 'kuery', - }).error - ).toBeFalsy(); + }).value.references + ).toEqual([]); }); - test('does not default references to an array', () => { + test('references cannot be numbers', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate< + Partial> & { references: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -390,47 +375,60 @@ describe('update rules schema', () => { type: 'query', query: 'some-query', language: 'kuery', - }).value.references - ).toEqual(undefined); + references: [5], + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); }); - test('does not default interval', () => { + test('indexes cannot be numbers', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - }).value.interval - ).toEqual(undefined); + updateRulesSchema.validate> & { index: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + } + ).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); }); - test('does not default max signal', () => { + test('defaults interval to 5 min', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', - interval: '5m', type: 'query', - }).value.max_signals - ).toEqual(undefined); + }).value.interval + ).toEqual('5m'); }); - test('references cannot be numbers', () => { + test('defaults max signals to 100', () => { expect( - updateRulesSchema.validate< - Partial> & { references: number[] } - >({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -439,41 +437,34 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'query', - query: 'some-query', - language: 'kuery', - references: [5], - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); + }).value.max_signals + ).toEqual(100); }); - test('indexes cannot be numbers', () => { + test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - updateRulesSchema.validate< - Partial> & { index: number[] } - >({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', - index: [5], + index: ['index-1'], name: 'some-name', severity: 'low', interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', + type: 'saved_query', }).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); + ).toEqual('child "saved_id" fails because ["saved_id" is required]'); }); - test('saved_id is not required when type is saved_query and will validate without it', () => { + test('saved_id is required when type is saved_query and validates with it', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -482,14 +473,17 @@ describe('update rules schema', () => { severity: 'low', interval: '5m', type: 'saved_query', + saved_id: 'some id', }).error ).toBeFalsy(); }); - test('saved_id validates with saved_query', () => { + test('saved_query type can have filters with it', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -499,14 +493,19 @@ describe('update rules schema', () => { interval: '5m', type: 'saved_query', saved_id: 'some id', + filters: [], }).error ).toBeFalsy(); }); - test('saved_query type can have filters with it', () => { + test('filters cannot be a string', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -516,15 +515,17 @@ describe('update rules schema', () => { interval: '5m', type: 'saved_query', saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); + filters: 'some string', + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); }); test('language validates with kuery', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -542,8 +543,10 @@ describe('update rules schema', () => { test('language validates with lucene', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', description: 'some description', from: 'now-5m', to: 'now', @@ -561,8 +564,10 @@ describe('update rules schema', () => { test('language does not validate with something made up', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -580,8 +585,10 @@ describe('update rules schema', () => { test('max_signals cannot be negative', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -600,8 +607,10 @@ describe('update rules schema', () => { test('max_signals cannot be zero', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -620,8 +629,10 @@ describe('update rules schema', () => { test('max_signals can be 1', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -638,42 +649,12 @@ describe('update rules schema', () => { ).toBeFalsy(); }); - test('meta can be updated', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - meta: { whateverYouWant: 'anything_at_all' }, - }).error - ).toBeFalsy(); - }); - - test('You cannot update meta as a string', () => { - expect( - updateRulesSchema.validate< - Partial & { meta: string }> - >({ - id: 'rule-1', - meta: 'should not work', - }).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('filters cannot be a string', () => { + test('You can optionally send in an array of tags', () => { expect( - updateRulesSchema.validate< - Partial & { filters: string }> - >({ + updateRulesSchema.validate>({ rule_id: 'rule-1', - type: 'query', - filters: 'some string', - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('threat is not defaulted to empty array on update', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -686,15 +667,18 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).value.threat - ).toBe(undefined); + tags: ['tag_1', 'tag_2'], + }).error + ).toBeFalsy(); }); - test('threat is not defaulted to undefined on update with empty array', () => { + test('You cannot send in an array of tags that are numbers', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', + updateRulesSchema.validate> & { tags: number[] }>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], @@ -706,32 +690,23 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - threat: [], - }).value.threat - ).toMatchObject([]); - }); - - test('threat is valid when updated with all sub-objects', () => { - const expected: ThreatParams[] = [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ]; + tags: [0, 1, 2], + }).error.message + ).toEqual( + 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]' + ); + }); + + test('You cannot send in an array of threat that are missing "framework"', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate< + Partial> & { + threat: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -746,7 +721,6 @@ describe('update rules schema', () => { max_signals: 1, threat: [ { - framework: 'fake', tactic: { id: 'fakeId', name: 'fakeName', @@ -761,18 +735,22 @@ describe('update rules schema', () => { ], }, ], - }).value.threat - ).toMatchObject(expected); + }).error.message + ).toEqual( + 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); }); - test('threat is invalid when updated with missing property framework', () => { + test('You cannot send in an array of threat that are missing "tactic"', () => { expect( updateRulesSchema.validate< - Partial> & { - threat: Array>>; + Partial> & { + threat: Array>>; } >({ - id: 'rule-1', + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -787,11 +765,7 @@ describe('update rules schema', () => { max_signals: 1, threat: [ { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, + framework: 'fake', technique: [ { id: 'techniqueId', @@ -803,18 +777,20 @@ describe('update rules schema', () => { ], }).error.message ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' ); }); - test('threat is invalid when updated with missing tactic sub-object', () => { + test('You cannot send in an array of threat that are missing "technique"', () => { expect( updateRulesSchema.validate< - Partial> & { - threat: Array>>; + Partial> & { + threat: Array>>; } >({ - id: 'rule-1', + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -830,30 +806,52 @@ describe('update rules schema', () => { threat: [ { framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, }, ], }).error.message ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' ); }); - test('threat is invalid when updated with missing technique', () => { + test('You can optionally send in an array of false positives', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: ['false_1', 'false_2'], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of false positives that are numbers', () => { expect( updateRulesSchema.validate< - Partial> & { - threat: Array>>; - } + Partial> & { false_positives: number[] } >({ - id: 'rule-1', + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', + false_positives: [5, 4], from: 'now-5m', to: 'now', index: ['index-1'], @@ -865,26 +863,201 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - }, - ], }).error.message ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' + 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' ); }); + test('You cannot set the immutable when trying to create a rule', () => { + expect( + updateRulesSchema.validate< + Partial> & { immutable: number } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: 5, + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error.message + ).toEqual('"immutable" is not allowed'); + }); + + test('You cannot set the risk_score to 101', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 101, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); + }); + + test('You cannot set the risk_score to -1', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: -1, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); + }); + + test('You can set the risk_score to 0', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 0, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set the risk_score to 100', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 100, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set meta to any object you want', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }).error + ).toBeFalsy(); + }); + + test('You cannot create meta as a string', () => { + expect( + updateRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: 'should not work', + }).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); + }); + + test('You can omit the query string when filters are present', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + test('validates with timeline_id and timeline_title', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -892,18 +1065,22 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', - timeline_title: 'some-title', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }).error ).toBeFalsy(); }); test('You cannot omit timeline_title when timeline_id is present', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -911,17 +1088,21 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); }); test('You cannot have a null value for timeline_title when timeline_id is present', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -929,9 +1110,11 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'timeline-id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', timeline_title: null, }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); @@ -939,8 +1122,10 @@ describe('update rules schema', () => { test('You cannot have empty string for timeline_title when timeline_id is present', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -948,9 +1133,11 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', timeline_title: '', }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); @@ -958,8 +1145,10 @@ describe('update rules schema', () => { test('You cannot have timeline_title with an empty timeline_id', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -967,8 +1156,10 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', timeline_id: '', timeline_title: 'some-title', }).error.message @@ -977,8 +1168,10 @@ describe('update rules schema', () => { test('You cannot have timeline_title without timeline_id', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, description: 'some description', from: 'now-5m', to: 'now', @@ -986,17 +1179,55 @@ describe('update rules schema', () => { name: 'some-name', severity: 'low', interval: '5m', - type: 'saved_query', - saved_id: 'some id', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', timeline_title: 'some-title', }).error.message ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); }); + test('The default for "from" will be "now-6m"', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).value.from + ).toEqual('now-6m'); + }); + + test('The default for "to" will be "now"', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + name: 'some-name', + severity: 'low', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + version: 1, + }).value.to + ).toEqual('now'); + }); + test('You cannot set the severity to a value other than low, medium, high, or critical', () => { expect( - updateRulesSchema.validate>({ - id: 'rule-1', + updateRulesSchema.validate>({ + rule_id: 'rule-1', risk_score: 50, description: 'some description', name: 'some-name', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index 3aa8e007a8cbd..3e5a608d6b657 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -37,31 +37,44 @@ import { } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; + +/** + * This almost identical to the create_rules_schema except for a few details. + * - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented + * This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in + * this will update the rule's version number. + * - id is on here because you can pass in an id to update using it instead of rule_id. + */ export const updateRulesSchema = Joi.object({ - description, - enabled, - false_positives, + description: description.required(), + enabled: enabled.default(true), + id, + false_positives: false_positives.default([]), filters, - from, + from: from.default('now-6m'), rule_id, - id, index, - interval, - query: query.allow(''), - language, + interval: interval.default('5m'), + query: query.allow('').default(''), + language: language.default('kuery'), output_index, - saved_id, + saved_id: saved_id.when('type', { + is: 'saved_query', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), timeline_id, timeline_title, meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - references, + risk_score: risk_score.required(), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), + name: name.required(), + severity: severity.required(), + tags: tags.default([]), + to: to.default('now'), + type: type.required(), + threat: threat.default([]), + references: references.default([]), version, }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index beef8b4199c15..cc02a6a02962a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -36,7 +36,13 @@ export const createReadTagsRoute: Hapi.ServerRoute = { }); return tags; } catch (err) { - return transformError(err); + const error = transformError(err); + return headers + .response({ + message: error.message, + status_code: error.statusCode, + }) + .code(error.statusCode); } }, }; 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 index ffd0c791c5bb6..3e3ccfe5babef 100644 --- 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 @@ -18,51 +18,69 @@ import { describe('utils', () => { describe('transformError', () => { - test('returns boom if it is a boom object', () => { - const boom = new Boom(''); + test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { + const boom = new Boom('some boom message'); const transformed = transformError(boom); - expect(transformed).toBe(boom); + expect(transformed).toEqual({ + message: 'An internal server error occurred', + statusCode: 500, + }); }); - test('returns a boom if it is some non boom object that has a statusCode', () => { + test('returns transformed output if it is some non boom object that has a statusCode', () => { const error: Error & { statusCode?: number } = { statusCode: 403, name: 'some name', message: 'some message', }; const transformed = transformError(error); - expect(Boom.isBoom(transformed)).toBe(true); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); }); - test('returns a boom with the message set', () => { + test('returns a transformed message with the message set and statusCode', () => { const error: Error & { statusCode?: number } = { statusCode: 403, name: 'some name', message: 'some message', }; const transformed = transformError(error); - expect(transformed.message).toBe('some message'); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 403, + }); }); - test('does not return a boom if it is some non boom object but it does not have a status Code.', () => { + test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { const error: Error = { name: 'some name', message: 'some message', }; const transformed = transformError(error); - expect(Boom.isBoom(transformed)).toBe(false); + expect(transformed).toEqual({ + message: 'some message', + statusCode: 500, + }); }); - test('it detects a TypeError and returns a Boom', () => { + test('it detects a TypeError and returns a status code of 400 from that particular error type', () => { const error: TypeError = new TypeError('I have a type error'); const transformed = transformError(error); - expect(Boom.isBoom(transformed)).toBe(true); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); }); test('it detects a TypeError and returns a Boom status of 400', () => { const error: TypeError = new TypeError('I have a type error'); - const transformed = transformError(error) as Boom; - expect(transformed.output.statusCode).toBe(400); + const transformed = transformError(error); + expect(transformed).toEqual({ + message: 'I have a type error', + statusCode: 400, + }); }); }); 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 index 416c76b5d4eb5..af78f60f16ae4 100644 --- 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 @@ -8,20 +8,37 @@ import Boom from 'boom'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { ServerFacade, RequestFacade } from '../../../types'; -export const transformError = (err: Error & { statusCode?: number }) => { +export interface OutputError { + message: string; + statusCode: number; +} + +export const transformError = (err: Error & { statusCode?: number }): OutputError => { if (Boom.isBoom(err)) { - return err; + return { + message: err.output.payload.message, + statusCode: err.output.statusCode, + }; } else { if (err.statusCode != null) { - return new Boom(err.message, { statusCode: err.statusCode }); + return { + message: err.message, + statusCode: err.statusCode, + }; } else if (err instanceof TypeError) { // allows us to throw type errors instead of booms in some conditions // where we don't want to mingle Boom with the rest of the code - return new Boom(err.message, { statusCode: 400 }); + return { + message: err.message, + statusCode: 400, + }; } else { // natively return the err and allow the regular framework // to deal with the error when it is a non Boom - return err; + return { + message: err.message ?? '(unknown error message)', + statusCode: 500, + }; } } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 82fe16882882e..ae28e2d35d1ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Alert } from '../../../../../alerting/server/types'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; @@ -37,7 +38,7 @@ export const createRules = ({ type, references, version, -}: CreateRuleParams) => { +}: CreateRuleParams): Promise => { return alertsClient.create({ data: { name, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts new file mode 100644 index 0000000000000..f560b67cdc587 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -0,0 +1,151 @@ +/* + * 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 { defaults } from 'lodash/fp'; +import { PartialAlert } from '../../../../../alerting/server/types'; +import { readRules } from './read_rules'; +import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { addTags } from './add_tags'; +import { ruleStatusSavedObjectType } from './saved_object_mappings'; +import { calculateVersion, calculateName, calculateInterval } from './utils'; + +export const patchRules = async ({ + alertsClient, + actionsClient, // TODO: Use this whenever we add feature support for different action types + savedObjectsClient, + description, + falsePositives, + enabled, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + from, + immutable, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + version, +}: PatchRuleParams): Promise => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { + return null; + } + + const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { + description, + falsePositives, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + from, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + version, + }); + + const nextParams = defaults( + { + ...rule.params, + }, + { + description, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + index, + maxSignals, + riskScore, + severity, + threat, + to, + type, + references, + version: calculatedVersion, + } + ); + + const update = await alertsClient.update({ + id: rule.id, + data: { + tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), + name: calculateName({ updatedName: name, originalName: rule.name }), + schedule: { + interval: calculateInterval(interval, rule.schedule.interval), + }, + actions: rule.actions, + params: nextParams, + }, + }); + + if (rule.enabled && enabled === false) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled === true) { + await alertsClient.enable({ id: rule.id }); + const ruleCurrentStatus = savedObjectsClient + ? await savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }) + : null; + // set current status for this rule to be 'going to run' + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = 'going to run'; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } + } else { + // enabled is null or undefined and we do not touch the rule + } + + if (enabled != null) { + return { ...update, enabled }; + } else { + return update; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index c15a08c0871d9..95466b5968f2a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -20,7 +20,12 @@ import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; import { RequestFacade } from '../../../types'; import { Alert } from '../../../../../alerting/server/types'; -export type UpdateRuleAlertParamsRest = Partial & { +export type PatchRuleAlertParamsRest = Partial & { + id: string | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; +}; + +export type UpdateRuleAlertParamsRest = RuleAlertParamsRest & { id: string | undefined; rule_id: RuleAlertParams['ruleId'] | undefined; }; @@ -34,6 +39,14 @@ export interface FindParamsRest { filter: string; } +export interface PatchRulesRequest extends RequestFacade { + payload: PatchRuleAlertParamsRest; +} + +export interface BulkPatchRulesRequest extends RequestFacade { + payload: PatchRuleAlertParamsRest[]; +} + export interface UpdateRulesRequest extends RequestFacade { payload: UpdateRuleAlertParamsRest; } @@ -153,7 +166,12 @@ export interface Clients { actionsClient: ActionsClient; } -export type UpdateRuleParams = Partial & { +export type PatchRuleParams = Partial & { + id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; +} & Clients; + +export type UpdateRuleParams = RuleAlertParams & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 9e4b3a2e54806..b4d98b5354fb6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; -import { updateRules } from './update_rules'; +import { patchRules } from './patch_rules'; import { PrepackagedRules } from '../types'; export const updatePrepackagedRules = async ( @@ -45,7 +45,7 @@ export const updatePrepackagedRules = async ( // Note: we do not pass down enabled as we do not want to suddenly disable // or enable rules on the user when they were not expecting it if a rule updates - return updateRules({ + return patchRules({ alertsClient, actionsClient, description, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 634c0d5a52cb1..1dc5d8429fab8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -4,79 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaults, pickBy, isEmpty } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; -import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; import { addTags } from './add_tags'; import { ruleStatusSavedObjectType } from './saved_object_mappings'; - -export const calculateInterval = ( - interval: string | undefined, - ruleInterval: string | undefined -): string => { - if (interval != null) { - return interval; - } else if (ruleInterval != null) { - return ruleInterval; - } else { - return '5m'; - } -}; - -export const calculateVersion = ( - immutable: boolean, - currentVersion: number, - updateProperties: Partial> -): number => { - // early return if we are pre-packaged/immutable rule to be safe. We are never responsible - // for changing the version number of an immutable. Immutables are only responsible for changing - // their own version number. This would be really bad if an immutable version number is bumped by us - // due to a bug, hence the extra check and early bail if that is detected. - if (immutable === true) { - if (updateProperties.version != null) { - // we are an immutable rule but we are asking to update the version number so go ahead - // and update it to what is asked. - return updateProperties.version; - } else { - // we are immutable and not asking to update the version number so return the existing version - return currentVersion; - } - } - - // white list all properties but the enabled/disabled flag. We don't want to auto-increment - // the version number if only the enabled/disabled flag is being set. Likewise if we get other - // properties we are not expecting such as updatedAt we do not to cause a version number bump - // on that either. - const removedNullValues = pickBy( - (value: unknown) => value != null, - updateProperties - ); - if (isEmpty(removedNullValues)) { - return currentVersion; - } else { - return currentVersion + 1; - } -}; - -export const calculateName = ({ - updatedName, - originalName, -}: { - updatedName: string | undefined; - originalName: string | undefined; -}): string => { - if (updatedName != null) { - return updatedName; - } else if (originalName != null) { - return originalName; - } else { - // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a rule name became null or undefined at - // some point since TypeScript allows it. - return 'untitled'; - } -}; +import { calculateVersion } from './utils'; export const updateRules = async ({ alertsClient, @@ -141,47 +74,40 @@ export const updateRules = async ({ version, }); - const nextParams = defaults( - { - ...rule.params, - }, - { - description, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - index, - maxSignals, - riskScore, - severity, - threat, - to, - type, - references, - version: calculatedVersion, - } - ); - const update = await alertsClient.update({ id: rule.id, data: { - tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), - name: calculateName({ updatedName: name, originalName: rule.name }), - schedule: { - interval: calculateInterval(interval, rule.schedule.interval), - }, + tags: addTags(tags, rule.params.ruleId, immutable), + name, + schedule: { interval }, actions: rule.actions, - params: nextParams, + params: { + description, + ruleId: rule.params.ruleId, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + index, + maxSignals, + riskScore, + severity, + threat, + to, + type, + references, + version: calculatedVersion, + }, }, }); + if (rule.enabled && enabled === false) { await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { @@ -204,13 +130,7 @@ export const updateRules = async ({ ...currentStatusToDisable.attributes, }); } - } else { - // enabled is null or undefined and we do not touch the rule } - if (enabled != null) { - return { ...update, enabled }; - } else { - return update; - } + return { ...update, enabled }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts index 0d426fb03bd37..b7c36b20f44be 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateName, calculateVersion } from './update_rules'; -import { UpdateRuleParams } from './types'; +import { calculateInterval, calculateVersion, calculateName } from './utils'; +import { PatchRuleParams } from './types'; -describe('update_rules', () => { +describe('utils', () => { describe('#calculateInterval', () => { test('given a undefined interval, it returns the ruleInterval ', () => { const interval = calculateInterval(undefined, '10m'); @@ -44,7 +44,7 @@ describe('update_rules', () => { test('returning an updated version number if not given an immutable but an updated falsy value', () => { expect( - calculateVersion(false, 1, ({ description: false } as unknown) as UpdateRuleParams) + calculateVersion(false, 1, ({ description: false } as unknown) as PatchRuleParams) ).toEqual(2); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts new file mode 100644 index 0000000000000..7d6091f6b97fa --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/utils.ts @@ -0,0 +1,75 @@ +/* + * 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 { pickBy, isEmpty } from 'lodash/fp'; +import { PatchRuleParams } from './types'; + +export const calculateInterval = ( + interval: string | undefined, + ruleInterval: string | undefined +): string => { + if (interval != null) { + return interval; + } else if (ruleInterval != null) { + return ruleInterval; + } else { + return '5m'; + } +}; + +export const calculateVersion = ( + immutable: boolean, + currentVersion: number, + updateProperties: Partial> +): number => { + // early return if we are pre-packaged/immutable rule to be safe. We are never responsible + // for changing the version number of an immutable. Immutables are only responsible for changing + // their own version number. This would be really bad if an immutable version number is bumped by us + // due to a bug, hence the extra check and early bail if that is detected. + if (immutable === true) { + if (updateProperties.version != null) { + // we are an immutable rule but we are asking to update the version number so go ahead + // and update it to what is asked. + return updateProperties.version; + } else { + // we are immutable and not asking to update the version number so return the existing version + return currentVersion; + } + } + + // white list all properties but the enabled/disabled flag. We don't want to auto-increment + // the version number if only the enabled/disabled flag is being set. Likewise if we get other + // properties we are not expecting such as updatedAt we do not to cause a version number bump + // on that either. + const removedNullValues = pickBy( + (value: unknown) => value != null, + updateProperties + ); + if (isEmpty(removedNullValues)) { + return currentVersion; + } else { + return currentVersion + 1; + } +}; + +export const calculateName = ({ + updatedName, + originalName, +}: { + updatedName: string | undefined; + originalName: string | undefined; +}): string => { + if (updatedName != null) { + return updatedName; + } else if (originalName != null) { + return originalName; + } else { + // You really should never get to this point. This is a fail safe way to send back + // the name of "untitled" just in case a rule name became null or undefined at + // some point since TypeScript allows it. + return 'untitled'; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh new file mode 100755 index 0000000000000..8094d9bad552c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule.sh @@ -0,0 +1,31 @@ +#!/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 + +# Uses a default if no argument is specified +RULES=(${@:-./rules/patches/simplest_updated_name.json}) + +# Example: ./patch_rule.sh +# Example: ./patch_rule.sh ./rules/patches/simplest_updated_name.json +# Example glob: ./patch_rule.sh ./rules/patches/* +for RULE in "${RULES[@]}" +do { + [ -e "$RULE" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ + -d @${RULE} \ + | jq .; +} & +done + +wait diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh new file mode 100755 index 0000000000000..3ae32445433ad --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/patch_rule_bulk.sh @@ -0,0 +1,22 @@ +#!/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 + +# Uses a default if no argument is specified +RULES=${1:-./rules/bulk/patch_names.json} + +# Example: ./patch_rule_bulk.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_update \ + -d @${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json index 9e5328ffabe2e..ef172acde3807 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json @@ -2,6 +2,7 @@ { "name": "Simplest Query Number 1", "description": "Simplest query with the least amount of fields required", + "rule_id": "query-rule-id-1", "risk_score": 1, "severity": "high", "type": "query", @@ -12,6 +13,7 @@ { "name": "Simplest Query Number 2", "description": "Simplest query with the least amount of fields required", + "rule_id": "query-rule-id-2", "risk_score": 2, "severity": "low", "type": "query", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/patch_names.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md new file mode 100644 index 0000000000000..bb47e4adfc56d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/README.md @@ -0,0 +1,25 @@ +These are example PATCH rules to see how to patch various parts of the rules. +You either have to use the id, or you have to use the rule_id in order to patch +the rules. rule_id acts as an external_id where you can patch rules across different +Kibana systems where id acts as a normal server generated id which is not normally shared +across different Kibana systems. + +The only thing you cannot patch is the `rule_id` or regular `id` of the system. If `rule_id` +is incorrect then you have to delete the rule completely and re-initialize it with the +correct `rule_id` + +First add all the examples from queries like so: + +```sh +./post_rule.sh ./rules/queries/*.json +``` + +Then to selectively patch a rule add the file of your choosing to patch: + +```sh +./patch_rule.sh ./rules/patches/.json +``` + +Take note that the ones with "id" must be changed to a GUID that only you know about through +a `./find_rules.sh`. For example to grab a GUID id off of the first found record that exists +you can do: `./find_rules.sh | jq '.data[0].id'` and then replace the id in `patches/simplest_update_risk_score_by_id.json` with that particular id to watch it happen. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json new file mode 100644 index 0000000000000..a94558143882b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/disable_rule.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "enabled": false +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json new file mode 100644 index 0000000000000..bfe7c7f546fc3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/enabled_rule.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "enabled": true +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json new file mode 100644 index 0000000000000..00966ddba7c7a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_id.json @@ -0,0 +1,4 @@ +{ + "id": "ade31ba8-dc49-4c18-b7f4-370b35df5f57", + "risk_score": 38 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json new file mode 100644 index 0000000000000..ad3c78183297d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_update_risk_score_by_rule_id.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "risk_score": 98 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json new file mode 100644 index 0000000000000..56c9f151dc712 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "name": "Changes only the name to this new value" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json new file mode 100644 index 0000000000000..72a535f0ef641 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_interval.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "interval": "6m" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json new file mode 100644 index 0000000000000..eb210cd8153d7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_query_everything.json @@ -0,0 +1,82 @@ +{ + "name": "Updates a query with all possible fields that can be updated", + "description": "Kitchen Sink (everything) query that has all possible fields filled out.", + "false_positives": [ + "https://www.example.com/some-article-about-a-false-positive", + "some text string about why another condition could be a false positive" + ], + "rule_id": "rule-id-everything", + "filters": [ + { + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + } + } + ], + "enabled": false, + "index": ["auditbeat-*", "filebeat-*"], + "interval": "5m", + "query": "user.name: root or user.name: admin", + "output_index": ".siem-signals-default", + "meta": { + "anything_you_want_ui_related_or_otherwise": { + "as_deep_structured_as_you_need": { + "any_data_type": {} + } + } + }, + "language": "kuery", + "risk_score": 1, + "max_signals": 100, + "tags": ["tag 1", "tag 2", "any tag you want"], + "to": "now", + "from": "now-6m", + "severity": "high", + "type": "query", + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0040", + "name": "impact", + "reference": "https://attack.mitre.org/tactics/TA0040/" + }, + "technique": [ + { + "id": "T1499", + "name": "endpoint denial of service", + "reference": "https://attack.mitre.org/techniques/T1499/" + } + ] + }, + { + "framework": "Some other Framework you want", + "tactic": { + "id": "some-other-id", + "name": "Some other name", + "reference": "https://example.com" + }, + "technique": [ + { + "id": "some-other-id", + "name": "some other technique name", + "reference": "https://example.com" + } + ] + } + ], + "references": [ + "http://www.example.com/some-article-about-attack", + "Some plain text string here explaining why this is a valid thing to look out for" + ], + "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title", + "version": 42 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json new file mode 100644 index 0000000000000..be833105792c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_tags.json @@ -0,0 +1,4 @@ +{ + "rule_id": "tags-query", + "tags": ["tag_3"] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json new file mode 100644 index 0000000000000..27dee7dd81463 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_timelineid.json @@ -0,0 +1,5 @@ +{ + "rule_id": "query-rule-id", + "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json new file mode 100644 index 0000000000000..8df63dd22bf9a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_version.json @@ -0,0 +1,4 @@ +{ + "rule_id": "query-rule-id", + "version": 500 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md index 97a5d31bb0133..5fdf0faa122e9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/README.md @@ -1,4 +1,4 @@ -These are example PUT rules to see how to update various parts of the rules. +These are example update rules to see how to update various parts of the rules. You either have to use the id, or you have to use the rule_id in order to update the rules. rule_id acts as an external_id where you can update rules across different Kibana systems where id acts as a normal server generated id which is not normally shared @@ -14,7 +14,7 @@ First add all the examples from queries like so: ./post_rule.sh ./rules/queries/*.json ``` -Then to selectively update a rule add the file of your choosing to update: +Then to selectively update a rule add the file of your choosing to patch: ```sh ./update_rule.sh ./rules/updates/.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json index a94558143882b..8752d66e4a0dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/disable_rule.json @@ -1,4 +1,10 @@ { + "name": "Some new name", + "description": "Changing the name and disabling this query", "rule_id": "query-rule-id", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", "enabled": false } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json index bfe7c7f546fc3..3556e2c94da48 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/enabled_rule.json @@ -1,4 +1,10 @@ { + "name": "Some new name", + "description": "Changing the name and enabling this query", "rule_id": "query-rule-id", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", "enabled": true } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json index 00966ddba7c7a..847c7480ef6b5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_id.json @@ -1,4 +1,9 @@ { - "id": "ade31ba8-dc49-4c18-b7f4-370b35df5f57", - "risk_score": 38 + "id": "1100ba1b-ed7e-4755-b326-1f6fa2bd6758", + "name": "Some new name", + "description": "Changing the name and changing the risk score", + "risk_score": 38, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json index ad3c78183297d..5c1e71e3833a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_update_risk_score_by_rule_id.json @@ -1,4 +1,9 @@ { "rule_id": "query-rule-id", - "risk_score": 98 + "name": "Some new name", + "description": "Changing the name and changing the risk score", + "risk_score": 98, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json index 56c9f151dc712..ef086743e07f4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/simplest_updated_name.json @@ -1,4 +1,9 @@ { + "name": "Changes only the name to this new value", + "description": "Query with a rule_id that acts like an external id", "rule_id": "query-rule-id", - "name": "Changes only the name to this new value" + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json index 72a535f0ef641..80bf306fe36b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_interval.json @@ -1,4 +1,10 @@ { "rule_id": "query-rule-id", - "interval": "6m" + "interval": "6m", + "name": "Some new name", + "description": "Changing the interval and risk score", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 0 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json index be833105792c6..4b9f773a1a4b0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_tags.json @@ -1,4 +1,10 @@ { - "rule_id": "tags-query", - "tags": ["tag_3"] + "rule_id": "query-rule-id", + "tags": ["tag_1", "tag_2", "tag_3"], + "name": "Some new name", + "description": "Adding tags and a few other updates such as name", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 10 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json index 27dee7dd81463..0fb8626fe3ce4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json @@ -1,5 +1,11 @@ { "rule_id": "query-rule-id", "timeline_id": "other-timeline-id", - "timeline_title": "other-timeline-title" + "timeline_title": "other-timeline-title", + "name": "Some new name", + "description": "Adding tags and a few other updates such as name", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 10 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json index 8df63dd22bf9a..4df935fb3f6b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_version.json @@ -1,4 +1,10 @@ { "rule_id": "query-rule-id", - "version": 500 + "version": 500, + "name": "Changes the version to arbitrary number", + "description": "Changes the version to some arbitrary number", + "type": "query", + "query": "user.name: root or user.name: admin", + "severity": "low", + "risk_score": 10 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh index aa22db965664a..22bc4fb7bf584 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -14,7 +14,7 @@ RULES=(${@:-./rules/updates/simplest_updated_name.json}) # Example: ./update_rule.sh # Example: ./update_rule.sh ./rules/updates/simplest_updated_name.json -# Example glob: ./post_rule.sh ./rules/updates/* +# Example glob: ./update_rule.sh ./rules/updates/* for RULE in "${RULES[@]}" do { [ -e "$RULE" ] || continue diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh index c9cb0676821c5..11fb8d0b6f81c 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -RULES=${1:-./rules/bulk/update_names.json} +RULES=${1:-./rules/bulk/multiple_simplest_queries.json} # Example: ./update_rule_bulk.sh curl -s -k \ diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index e984c23385508..8ef87f2acc7a4 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -15,6 +15,7 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/api_integration/config.js'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), + require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.js'), require.resolve('../test/kerberos_api_integration/config'), require.resolve('../test/kerberos_api_integration/anonymous_access.config'), diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts new file mode 100644 index 0000000000000..3e626e945d7d8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -0,0 +1,92 @@ +/* + * 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 path from 'path'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.server-log', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +// eslint-disable-next-line import/no-default-export +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers, + services, + junit: { + reportName: 'X-Pack Detection Engine API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'aad')}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts @@ -0,0 +1,11 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/detection_engine_api_integration/common/services.ts b/x-pack/test/detection_engine_api_integration/common/services.ts new file mode 100644 index 0000000000000..a927a31469bab --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/common/services.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { services } from '../../api_integration/services'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts new file mode 100644 index 0000000000000..081b901c47fc3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts @@ -0,0 +1,14 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, +}); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts new file mode 100644 index 0000000000000..5e09013fb32a3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -0,0 +1,92 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('add_prepackaged_rules', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body).to.eql({ + message: + 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', + status_code: 400, + }); + }); + }); + + describe('creating prepackaged rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should contain two output keys of rules_installed and rules_updated', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql(['rules_installed', 'rules_updated']); + }); + + it('should create the prepackaged rules and return a count greater than zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_installed).to.be.greaterThan(0); + }); + + it('should create the prepackaged rules that the rules_updated is of size zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_updated).to.eql(0); + }); + + it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_installed).to.eql(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts new file mode 100644 index 0000000000000..d6a238e5b0940 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -0,0 +1,97 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('create_rules', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('creating rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should create a single rule without a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(409); + + expect(body).to.eql({ + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts new file mode 100644 index 0000000000000..dfa297c85dfb8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -0,0 +1,124 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('create_rules_bulk', () => { + describe('validation errors', () => { + it('should give a 200 even if the index does not exist as all bulks return a 200 but have an error of 409 bad request in the body', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); + }); + + describe('creating rules in bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should create a single rule without a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRuleWithoutRuleId()]) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + // TODO: This is a valid issue and will be fixed in an upcoming PR and then enabled once that PR is merged + it.skip('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule(), getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: 'Conflict', + message: 'rule_id: "rule-1" already exists', + statusCode: 409, + }, + ]); + }); + + it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id that already exists', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'true') + .send([getSimpleRule()]) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) + .set('kbn-xsrf', 'foo') + .send([getSimpleRule()]) + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts new file mode 100644 index 0000000000000..ee34e5e261987 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -0,0 +1,123 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('delete_rules', () => { + describe('deleting rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // create a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // delete the rule by its rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule where the rule_id is auto-generated + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its auto-generated rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=${bodyWithCreatedRule.rule_id}`) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its auto-generated id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=${bodyWithCreatedRule.id}`) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .set('kbn-xsrf', 'true') + .query() + .expect(404); + + expect(body).to.eql({ + message: 'id: "fake_id" not found', + status_code: 404, + }); + }); + + it('should return an error if the rule_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`) + .set('kbn-xsrf', 'true') + .query() + .expect(404); + + expect(body).to.eql({ + message: 'rule_id: "fake_id" not found', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts new file mode 100644 index 0000000000000..5a1c178f6b211 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -0,0 +1,293 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('delete_rules_bulk', () => { + describe('deleting rules bulk using DELETE', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // add a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete the rule in bulk + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1' }]) + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule without a rule_id + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its rule_id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: bodyWithCreatedRule.rule_id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its id + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should return an error if the id does not exist when trying to delete an id', async () => { + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO This is a known issue where it should be id and not rule_id + }, + ]); + }); + + it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => { + // add the rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + getSimpleRuleOutputWithoutRuleId(), + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + }); + + // This is a repeat of the tests above but just using POST instead of DELETE + describe('deleting rules bulk using POST', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should delete a single rule with a rule_id', async () => { + // add a rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'foo') + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete the rule in bulk + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1' }]) + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should delete a single rule using an auto generated rule_id', async () => { + // add a rule without a rule_id + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + // delete that rule by its rule_id + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: bodyWithCreatedRule.rule_id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should delete a single rule using an auto generated id', async () => { + // add a rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // delete that rule by its id + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ rule_id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should return an error if the id does not exist when trying to delete an id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + expect(body).to.eql([ + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO This is a known issue where it should be id and not rule_id + }, + ]); + }); + + it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => { + // add the rule + const { body: bodyWithCreatedRule } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .set('kbn-xsrf', 'true') + .query() + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + getSimpleRuleOutputWithoutRuleId(), + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts new file mode 100644 index 0000000000000..8882448dfcdc2 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts @@ -0,0 +1,134 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + binaryToString, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('export_rules', () => { + describe('exporting rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should set the response content types to be expected', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .expect('Content-Type', 'application/ndjson') + .expect('Content-Disposition', 'attachment; filename="export.ndjson"'); + }); + + it('should export a single rule with a rule_id', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .parse(binaryToString); + + const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[0]); + const bodyToTest = removeServerGeneratedProperties(bodySplitAndParsed); + + expect(bodyToTest).to.eql(getSimpleRuleOutput()); + }); + + it('should export a exported count with a single rule_id', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .parse(binaryToString); + + const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]); + + expect(bodySplitAndParsed).to.eql({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + }); + }); + + it('should export exactly two rules given two rules', async () => { + // post rule 1 + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // post rule 2 + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_export`) + .set('kbn-xsrf', 'true') + .send() + .query() + .expect(200) + .parse(binaryToString); + + const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); + const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); + const firstRule = removeServerGeneratedProperties(firstRuleParsed); + const secondRule = removeServerGeneratedProperties(secondRuleParsed); + + expect([firstRule, secondRule]).to.eql([ + getSimpleRuleOutput('rule-2'), + getSimpleRuleOutput('rule-1'), + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts new file mode 100644 index 0000000000000..82e506b23ca97 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts @@ -0,0 +1,98 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getComplexRule, + getComplexRuleOutput, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('find_rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should return an empty find body correctly if no rules are loaded', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + perPage: 20, + total: 0, + }); + }); + + it('should return a single rule when a single rule is loaded from a find with defaults added', async () => { + // add a single rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // query the single rule from _find + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send(); + + body.data = [removeServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getSimpleRuleOutput()], + page: 1, + perPage: 20, + total: 1, + }); + }); + + it('should return a single rule when a single rule is loaded from a find with everything for the rule added', async () => { + // add a single rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getComplexRule()) + .expect(200); + + // query and expect that we get back one record in the find + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send(); + + body.data = [removeServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getComplexRuleOutput()], + page: 1, + perPage: 20, + total: 1, + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts new file mode 100644 index 0000000000000..49cf150126fda --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts @@ -0,0 +1,100 @@ +/* + * 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 expect from '@kbn/expect'; + +import { + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_URL, +} from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, getSimpleRule } from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_prepackaged_rules_status', () => { + describe('getting prepackaged rules status', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should return expected JSON keys of the pre-packaged rules status', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql([ + 'rules_custom_installed', + 'rules_installed', + 'rules_not_installed', + 'rules_not_updated', + ]); + }); + + it('should return that rules_not_installed are greater than zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_not_installed).to.be.greaterThan(0); + }); + + it('should return that rules_custom_installed, rules_installed, and rules_not_updated are zero', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_custom_installed).to.eql(0); + expect(body.rules_installed).to.eql(0); + expect(body.rules_not_updated).to.eql(0); + }); + + it('should show that one custom rule is installed when a custom rule is added', async () => { + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_custom_installed).to.eql(1); + expect(body.rules_installed).to.eql(0); + expect(body.rules_not_updated).to.eql(0); + }); + + it('should show rules are installed when adding pre-packaged rules', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.rules_installed).to.be.greaterThan(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts new file mode 100644 index 0000000000000..e8fd1e4298c22 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -0,0 +1,331 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleAsNdjson, + getSimpleRuleOutput, + removeServerGeneratedProperties, + ruleToNdjson, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('import_rules', () => { + describe('importing rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should set the response content types to be expected', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(200); + }); + + it('should reject with an error if the file type is not that of a ndjson', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.txt') + .query() + .expect(400); + + expect(body).to.eql({ + status_code: 400, + message: 'Invalid file extension .txt', + }); + }); + + it('should report that it imported a simple rule successfully', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should be able to read an imported rule back out correctly', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput('rule-1')); + }); + + it('should be able to import two rules', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 2, + }); + }); + + it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], // TODO: This should have a conflict within it as an error rather than an empty array + success: true, + success_count: 1, + }); + }); + + it('should NOT report a conflict if there is an attempt to import two rules with the same rule_id and overwrite is set to true', async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 0, + }); + }); + + it('should NOT report a conflict if there is an attempt to import a rule with a rule_id that already exists and overwrite is set to true', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + }); + }); + + it('should overwrite an existing rule if overwrite is set to true', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const simpleRule = getSimpleRule('rule-1'); + simpleRule.name = 'some other name'; + const ndjson = ruleToNdjson(simpleRule); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) + .set('kbn-xsrf', 'true') + .attach('file', ndjson, 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + const ruleOutput = getSimpleRuleOutput('rule-1'); + ruleOutput.name = 'some other name'; + ruleOutput.version = 2; + expect(bodyToCompare).to.eql(ruleOutput); + }); + + it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + ], + success: false, + success_count: 2, + }); + }); + + it('should report a mix of conflicts and a mix of successes', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .query() + .expect(200); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .query() + .expect(200); + + expect(body).to.eql({ + errors: [ + { + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', + }, + { + error: { + message: 'rule_id: "rule-2" already exists', + status_code: 409, + }, + rule_id: 'rule-2', + }, + ], + success: false, + success_count: 1, + }); + }); + + it('should be able to correctly read back a mixed import of different rules even if some cause conflicts', async () => { + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson') + .query() + .expect(200); + + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_import`) + .set('kbn-xsrf', 'true') + .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson') + .query() + .expect(200); + + const { body: bodyOfRule1 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .send() + .expect(200); + + const { body: bodyOfRule2 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-2`) + .send() + .expect(200); + + const { body: bodyOfRule3 } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-3`) + .send() + .expect(200); + + const bodyToCompareOfRule1 = removeServerGeneratedProperties(bodyOfRule1); + const bodyToCompareOfRule2 = removeServerGeneratedProperties(bodyOfRule2); + const bodyToCompareOfRule3 = removeServerGeneratedProperties(bodyOfRule3); + + expect([bodyToCompareOfRule1, bodyToCompareOfRule2, bodyToCompareOfRule3]).to.eql([ + getSimpleRuleOutput('rule-1'), + getSimpleRuleOutput('rule-2'), + getSimpleRuleOutput('rule-3'), + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts new file mode 100644 index 0000000000000..ca6ef5b6cede9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled', function() { + this.tags('ciGroup1'); + + loadTestFile(require.resolve('./add_prepackaged_rules')); + loadTestFile(require.resolve('./create_rules')); + loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./delete_rules')); + loadTestFile(require.resolve('./delete_rules_bulk')); + loadTestFile(require.resolve('./export_rules')); + loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./get_prepackaged_rules_status')); + loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./update_rules')); + loadTestFile(require.resolve('./update_rules_bulk')); + loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./patch_rules')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts new file mode 100644 index 0000000000000..53a3d15690efc --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -0,0 +1,212 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleOutputWithoutRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('patch_rules', () => { + describe('patch rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should patch a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch a single rule property of name using the auto-generated rule_id', async () => { + // create a simple rule + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: createRuleBody.rule_id, name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutputWithoutRuleId(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ id: createdBody.id, name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change the version of a rule when it patches only enabled', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', enabled: false }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it patches enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false and another property + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', severity: 'low', enabled: false }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change other properties when it does patches', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's timeline_title + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'rule-1', name: 'some other name' }) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.timeline_title = 'some title'; + outputRule.timeline_id = 'some id'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should give a 404 if it is given a fake id', async () => { + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ id: 'fake_id', name: 'some other name' }) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should give a 404 if it is given a fake rule_id', async () => { + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: 'fake_id', name: 'some other name' }) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts new file mode 100644 index 0000000000000..3d14bc2db47b4 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -0,0 +1,356 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + getSimpleRuleOutputWithoutRuleId, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('patch_rules_bulk', () => { + describe('patch rules bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should patch a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch two rule properties of name using the two rules rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // patch both rule names + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { rule_id: 'rule-1', name: 'some other name' }, + { rule_id: 'rule-2', name: 'some other name' }, + ]) + .expect(200); + + const outputRule1 = getSimpleRuleOutput(); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutput('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should patch a single rule property of name using an id', async () => { + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: createRuleBody.id, name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should patch two rule properties of name using the two rules id', async () => { + // create a simple rule + const { body: createRule1 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + const { body: createRule2 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // patch both rule names + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { id: createRule1.id, name: 'some other name' }, + { id: createRule2.id, name: 'some other name' }, + ]) + .expect(200); + + const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should patch a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: createdBody.id, name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change the version of a rule when it patches only enabled', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', enabled: false }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it patches enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's enabled to false and another property + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', severity: 'low', enabled: false }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should not change other properties when it does patches', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch a simple rule's timeline_title + await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }]) + .expect(200); + + // patch a simple rule's name + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'rule-1', name: 'some other name' }]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.timeline_title = 'some title'; + outputRule.timeline_id = 'some id'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ id: 'fake_id', name: 'some other name' }]) + .expect(200); + + expect(body).to.eql([ + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([{ rule_id: 'fake_id', name: 'some other name' }]) + .expect(200); + + expect(body).to.eql([ + { + rule_id: 'fake_id', + error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, + }, + ]); + }); + + it('should patch one rule property and give an error about a second fake rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch one rule name and give a fake id for the second + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { rule_id: 'rule-1', name: 'some other name' }, + { rule_id: 'fake_id', name: 'some other name' }, + ]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should patch one rule property and give an error about a second fake id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // patch one rule name and give a fake id for the second + const { body } = await supertest + .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ + { id: createdBody.id, name: 'some other name' }, + { id: 'fake_id', name: 'some other name' }, + ]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO: This should be id and not rule_id in the codebase + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts new file mode 100644 index 0000000000000..2ea62b0756f73 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts @@ -0,0 +1,120 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + getSimpleRuleOutputWithoutRuleId, + getSimpleRuleWithoutRuleId, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('read_rules', () => { + describe('reading rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should be able to read a single rule using rule_id', async () => { + // create a simple rule to read + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should be able to read a single rule using id', async () => { + // create a simple rule to read + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutput()); + }); + + it('should be able to read a single rule with an auto-generated rule_id', async () => { + // create a simple rule to read + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRuleWithoutRuleId()) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${createRuleBody.rule_id}`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should return 404 if given a fake rule_id', async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=fake_id`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts new file mode 100644 index 0000000000000..92c78be72bf01 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -0,0 +1,221 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleOutputWithoutRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('update_rules', () => { + describe('update rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should update a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.rule_id = 'rule-1'; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update a single rule property of name using an auto-generated rule_id', async () => { + const rule = getSimpleRule('rule-1'); + delete rule.rule_id; + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.rule_id = createRuleBody.rule_id; + updatedRule.name = 'some other name'; + delete updatedRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutputWithoutRuleId(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule = getSimpleRule('rule-1'); + updatedRule.name = 'some other name'; + updatedRule.id = createdBody.id; + delete updatedRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it updates enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's enabled to false and another property + const updatedRule = getSimpleRule('rule-1'); + updatedRule.severity = 'low'; + updatedRule.enabled = false; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(updatedRule) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.timeline_title = 'some title'; + ruleUpdate.timeline_id = 'some id'; + + // update a simple rule's timeline_title + await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate) + .expect(200); + + const ruleUpdate2 = getSimpleRule('rule-1'); + ruleUpdate2.name = 'some other name'; + + // update a simple rule's name + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate2) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should give a 404 if it is given a fake id', async () => { + const simpleRule = getSimpleRule(); + simpleRule.id = 'fake_id'; + delete simpleRule.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'id: "fake_id" not found', + }); + }); + + it('should give a 404 if it is given a fake rule_id', async () => { + const simpleRule = getSimpleRule(); + simpleRule.rule_id = 'fake_id'; + delete simpleRule.id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(simpleRule) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'rule_id: "fake_id" not found', + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts new file mode 100644 index 0000000000000..4894cac2b2608 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -0,0 +1,386 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../legacy/plugins/siem/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + getSimpleRuleOutputWithoutRuleId, + removeServerGeneratedPropertiesIncludingRuleId, +} from './utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('update_rules_bulk', () => { + describe('update rules bulk', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should update a single rule property of name using a rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const updatedRule = getSimpleRule('rule-1'); + updatedRule.name = 'some other name'; + + // update a simple rule's name + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update two rule properties of name using the two rules rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.name = 'some other name'; + + const updatedRule2 = getSimpleRule('rule-2'); + updatedRule2.name = 'some other name'; + + // update both rule names + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1, updatedRule2]) + .expect(200); + + const outputRule1 = getSimpleRuleOutput(); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutput('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should update a single rule property of name using an id', async () => { + // create a simple rule + const { body: createRuleBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createRuleBody.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should update two rule properties of name using the two rules id', async () => { + // create a simple rule + const { body: createRule1 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // create a second simple rule + const { body: createRule2 } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-2')) + .expect(200); + + // update both rule names + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createRule1.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const updatedRule2 = getSimpleRule('rule-1'); + updatedRule2.id = createRule2.id; + updatedRule2.name = 'some other name'; + delete updatedRule2.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1, updatedRule2]) + .expect(200); + + const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + outputRule1.name = 'some other name'; + outputRule1.version = 2; + + const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + outputRule2.name = 'some other name'; + outputRule2.version = 2; + + const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); + const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + expect([bodyToCompare1, bodyToCompare2]).to.eql([outputRule1, outputRule2]); + }); + + it('should update a single rule property of name using the auto-generated id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's name + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.id = createdBody.id; + updatedRule1.name = 'some other name'; + delete updatedRule1.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change the version of a rule when it updates enabled and another property', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's enabled to false and another property + const updatedRule1 = getSimpleRule('rule-1'); + updatedRule1.severity = 'low'; + updatedRule1.enabled = false; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([updatedRule1]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.enabled = false; + outputRule.severity = 'low'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update a simple rule's timeline_title + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.timeline_title = 'some title'; + ruleUpdate.timeline_id = 'some id'; + + await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + // update a simple rule's name + const ruleUpdate2 = getSimpleRule('rule-1'); + ruleUpdate2.name = 'some other name'; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 3; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect(bodyToCompare).to.eql(outputRule); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.id = 'fake_id'; + delete ruleUpdate.rule_id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + expect(body).to.eql([ + { rule_id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + ]); + }); + + it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.rule_id = 'fake_id'; + delete ruleUpdate.id; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate]) + .expect(200); + + expect(body).to.eql([ + { + rule_id: 'fake_id', + error: { status_code: 404, message: 'rule_id: "fake_id" not found' }, + }, + ]); + }); + + it('should update one rule property and give an error about a second fake rule_id', async () => { + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + const ruleUpdate = getSimpleRule('rule-1'); + ruleUpdate.name = 'some other name'; + delete ruleUpdate.id; + + const ruleUpdate2 = getSimpleRule('fake_id'); + ruleUpdate2.name = 'some other name'; + delete ruleUpdate.id; + + // update one rule name and give a fake id for the second + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([ruleUpdate, ruleUpdate2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'rule_id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', + }, + ]); + }); + + it('should update one rule property and give an error about a second fake id', async () => { + // create a simple rule + const { body: createdBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleRule('rule-1')) + .expect(200); + + // update one rule name and give a fake id for the second + const rule1 = getSimpleRule(); + delete rule1.rule_id; + rule1.id = createdBody.id; + rule1.name = 'some other name'; + + const rule2 = getSimpleRule(); + delete rule2.rule_id; + rule2.id = 'fake_id'; + rule2.name = 'some other name'; + + const { body } = await supertest + .put(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) + .set('kbn-xsrf', 'true') + .send([rule1, rule2]) + .expect(200); + + const outputRule = getSimpleRuleOutput(); + outputRule.name = 'some other name'; + outputRule.version = 2; + + const bodyToCompare = removeServerGeneratedProperties(body[0]); + expect([bodyToCompare, body[1]]).to.eql([ + outputRule, + { + error: { + message: 'id: "fake_id" not found', + status_code: 404, + }, + rule_id: 'fake_id', // TODO: This should be id and not rule_id in the codebase + }, + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts new file mode 100644 index 0000000000000..b78073c0e737b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -0,0 +1,345 @@ +/* + * 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 { OutputRuleAlertRest } from '../../../../legacy/plugins/siem/server/lib/detection_engine/types'; +import { DETECTION_ENGINE_INDEX_URL } from '../../../../legacy/plugins/siem/common/constants'; + +/** + * This will remove server generated properties such as date times, etc... + * @param rule Rule to pass in to remove typical server generated properties + */ +export const removeServerGeneratedProperties = ( + rule: Partial +): Partial => { + const { + created_at, + updated_at, + id, + last_success_at, + last_success_message, + status, + status_date, + ...removedProperties + } = rule; + return removedProperties; +}; + +/** + * This will remove server generated properties such as date times, etc... including the rule_id + * @param rule Rule to pass in to remove typical server generated properties + */ +export const removeServerGeneratedPropertiesIncludingRuleId = ( + rule: Partial +): Partial => { + const ruleWithRemovedProperties = removeServerGeneratedProperties(rule); + const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties; + return additionalRuledIdRemoved; +}; + +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + rule_id: ruleId, + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', +}); + +/** + * This is a typical simple rule for testing that is easy for most basic testing + */ +export const getSimpleRuleWithoutRuleId = (): Partial => { + const simpleRule = getSimpleRule(); + const { rule_id, ...ruleWithoutId } = simpleRule; + return ruleWithoutId; +}; + +/** + * Useful for export_api testing to convert from a multi-part binary back to a string + * @param res Response + * @param callback Callback + */ +export const binaryToString = (res: any, callback: any): void => { + res.setEncoding('binary'); + res.data = ''; + res.on('data', (chunk: any) => { + res.data += chunk; + }); + res.on('end', () => { + callback(null, Buffer.from(res.data)); + }); +}; + +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults. + */ +export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => ({ + created_by: 'elastic', + description: 'Simple Rule Query', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: ruleId, + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 1, + name: 'Simple Rule Query', + query: 'user.name: root or user.name: admin', + references: [], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, +}); + +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults. + */ +export const getSimpleRuleOutputWithoutRuleId = ( + ruleId = 'rule-1' +): Partial => { + const rule = getSimpleRuleOutput(ruleId); + const { rule_id, ...ruleWithoutRuleId } = rule; + return ruleWithoutRuleId; +}; + +/** + * Remove all alerts from the .kibana index + * @param es The ElasticSearch handle + */ +export const deleteAllAlerts = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:alert', + waitForCompletion: true, + refresh: 'wait_for', + }); +}; + +/** + * Creates the signals index for use inside of beforeEach blocks of tests + * @param supertest The supertest client library + */ +export const createSignalsIndex = async (supertest: any): Promise => { + await supertest + .post(DETECTION_ENGINE_INDEX_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); +}; + +/** + * Deletes the signals index for use inside of afterEach blocks of tests + * @param supertest The supertest client library + */ +export const deleteSignalsIndex = async (supertest: any): Promise => { + await supertest + .delete(DETECTION_ENGINE_INDEX_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); +}; + +/** + * Given an array of rule_id strings this will return a ndjson buffer which is useful + * for testing uploads. + * @param ruleIds Array of strings of rule_ids + */ +export const getSimpleRuleAsNdjson = (ruleIds: string[]): Buffer => { + const stringOfRules = ruleIds.map(ruleId => { + const simpleRule = getSimpleRule(ruleId); + return JSON.stringify(simpleRule); + }); + return Buffer.from(stringOfRules.join('\n')); +}; + +/** + * Given a rule this will convert it to an ndjson buffer which is useful for + * testing upload features. + * @param rule The rule to convert to ndjson + */ +export const ruleToNdjson = (rule: Partial): Buffer => { + const stringified = JSON.stringify(rule); + return Buffer.from(`${stringified}\n`); +}; + +/** + * This will return a complex rule with all the outputs possible + * @param ruleId The ruleId to set which is optional and defaults to rule-1 + */ +export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Complex Rule Query', + description: 'Complex Rule Query', + false_positives: [ + 'https://www.example.com/some-article-about-a-false-positive', + 'some text string about why another condition could be a false positive', + ], + risk_score: 1, + rule_id: ruleId, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + enabled: false, + index: ['auditbeat-*', 'filebeat-*'], + interval: '5m', + output_index: '.siem-signals-default', + meta: { + anything_you_want_ui_related_or_otherwise: { + as_deep_structured_as_you_need: { + any_data_type: {}, + }, + }, + }, + max_signals: 10, + tags: ['tag 1', 'tag 2', 'any tag you want'], + to: 'now', + from: 'now-6m', + severity: 'high', + language: 'kuery', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + { + framework: 'Some other Framework you want', + tactic: { + id: 'some-other-id', + name: 'Some other name', + reference: 'https://example.com', + }, + technique: [ + { + id: 'some-other-id', + name: 'some other technique name', + reference: 'https://example.com', + }, + ], + }, + ], + references: [ + 'http://www.example.com/some-article-about-attack', + 'Some plain text string here explaining why this is a valid thing to look out for', + ], + timeline_id: 'timeline_id', + timeline_title: 'timeline_title', + version: 1, + query: 'user.name: root or user.name: admin', +}); + +/** + * This will return a complex rule with all the outputs possible + * @param ruleId The ruleId to set which is optional and defaults to rule-1 + */ +export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ + created_by: 'elastic', + name: 'Complex Rule Query', + description: 'Complex Rule Query', + false_positives: [ + 'https://www.example.com/some-article-about-a-false-positive', + 'some text string about why another condition could be a false positive', + ], + risk_score: 1, + rule_id: ruleId, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + enabled: false, + index: ['auditbeat-*', 'filebeat-*'], + immutable: false, + interval: '5m', + output_index: '.siem-signals-default', + meta: { + anything_you_want_ui_related_or_otherwise: { + as_deep_structured_as_you_need: { + any_data_type: {}, + }, + }, + }, + max_signals: 10, + tags: ['tag 1', 'tag 2', 'any tag you want'], + to: 'now', + from: 'now-6m', + severity: 'high', + language: 'kuery', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + { + framework: 'Some other Framework you want', + tactic: { + id: 'some-other-id', + name: 'Some other name', + reference: 'https://example.com', + }, + technique: [ + { + id: 'some-other-id', + name: 'some other technique name', + reference: 'https://example.com', + }, + ], + }, + ], + references: [ + 'http://www.example.com/some-article-about-attack', + 'Some plain text string here explaining why this is a valid thing to look out for', + ], + timeline_id: 'timeline_id', + timeline_title: 'timeline_title', + updated_by: 'elastic', + version: 1, + query: 'user.name: root or user.name: admin', +});