diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index 9ceb8e02864ec..a488db3f0c3d7 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -28,6 +28,7 @@ import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete 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'; const APP_ID = 'siem'; @@ -49,12 +50,16 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy updateRulesRoute(__legacy); deleteRulesRoute(__legacy); findRulesRoute(__legacy); + addPrepackedRulesRoute(__legacy); + getPrepackagedRulesStatusRoute(__legacy); createRulesBulkRoute(__legacy); updateRulesBulkRoute(__legacy); deleteRulesBulkRoute(__legacy); + importRulesRoute(__legacy); exportRulesRoute(__legacy); + findRulesStatusesRoute(__legacy); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts index 1e475f2014fa2..8bddd4a1ef456 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts @@ -10,9 +10,12 @@ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { savedObjectsClientMock } from '../../../../../../../../../src/core/server/mocks'; import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock'; +import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../../common/constants'; +import { ServerFacade } from '../../../../types'; const defaultConfig = { 'kibana.index': '.kibana', + [`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`]: '.siem-signals', }; const isKibanaConfig = (config: unknown): config is KibanaConfig => @@ -58,10 +61,10 @@ export const createMockServer = (config: Record = defaultConfig) server.decorate('request', 'getBasePath', () => '/s/default'); server.decorate('request', 'getActionsClient', () => actionsClient); server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin; + server.plugins.spaces = { getSpaceId: () => 'default' }; server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - return { - server, + server: server as ServerFacade & Hapi.Server, alertsClient, actionsClient, elasticsearch, @@ -82,7 +85,10 @@ export const createMockServerWithoutAlertClientDecoration = ( serverWithoutAlertClient.decorate('request', 'getBasePath', () => '/s/default'); serverWithoutAlertClient.decorate('request', 'getActionsClient', () => actionsClient); - return { serverWithoutAlertClient, actionsClient }; + return { + serverWithoutAlertClient: serverWithoutAlertClient as ServerFacade & Hapi.Server, + actionsClient, + }; }; export const createMockServerWithoutActionClientDecoration = ( @@ -98,7 +104,10 @@ export const createMockServerWithoutActionClientDecoration = ( serverWithoutActionClient.decorate('request', 'getBasePath', () => '/s/default'); serverWithoutActionClient.decorate('request', 'getAlertsClient', () => alertsClient); - return { serverWithoutActionClient, alertsClient }; + return { + serverWithoutActionClient: serverWithoutActionClient as ServerFacade & Hapi.Server, + alertsClient, + }; }; export const createMockServerWithoutActionOrAlertClientDecoration = ( @@ -111,6 +120,22 @@ export const createMockServerWithoutActionOrAlertClientDecoration = ( serverWithoutActionOrAlertClient.config = () => createMockKibanaConfig(config); return { - serverWithoutActionOrAlertClient, + serverWithoutActionOrAlertClient: serverWithoutActionOrAlertClient as ServerFacade & + Hapi.Server, }; }; + +export const getMockIndexName = () => + jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn().mockImplementationOnce(() => 'index-name'), + })); + +export const getMockEmptyIndex = () => + jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 0 } })), + })); + +export const getMockNonEmptyIndex = () => + jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 1 } })), + })); 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 01f30a3ebbdea..30a8d9d935128 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 @@ -15,6 +15,7 @@ import { DETECTION_ENGINE_QUERY_SIGNALS_URL, INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY, + DETECTION_ENGINE_PREPACKAGED_URL, } from '../../../../../common/constants'; import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { RuleAlertParamsRest } from '../../types'; @@ -157,7 +158,17 @@ export const getDeleteAsPostBulkRequest = (): ServerInjectOptions => ({ export const getPrivilegeRequest = (): ServerInjectOptions => ({ method: 'GET', - url: `${DETECTION_ENGINE_PRIVILEGES_URL}`, + url: DETECTION_ENGINE_PRIVILEGES_URL, +}); + +export const addPrepackagedRulesRequest = (): ServerInjectOptions => ({ + method: 'PUT', + url: DETECTION_ENGINE_PREPACKAGED_URL, +}); + +export const getPrepackagedRulesStatusRequest = (): ServerInjectOptions => ({ + method: 'GET', + url: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, }); export interface FindHit { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 54872f80a4c6d..1ea681afb7949 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -7,7 +7,6 @@ import { createMockServer } from '../__mocks__/_mock_server'; import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; import { readPrivilegesRoute } from './read_privileges_route'; -import { ServerFacade } from '../../../../types'; import * as myUtils from '../utils'; describe('read_privileges', () => { @@ -19,7 +18,7 @@ describe('read_privileges', () => { elasticsearch.getCluster = jest.fn(() => ({ callWithRequest: jest.fn(() => getMockPrivileges()), })); - readPrivilegesRoute((server as unknown) as ServerFacade); + readPrivilegesRoute(server); }); afterEach(() => { 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 new file mode 100644 index 0000000000000..ed193b6473a9e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -0,0 +1,138 @@ +/* + * 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, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, + getMockEmptyIndex, + getMockNonEmptyIndex, +} from '../__mocks__/_mock_server'; +import { createRulesRoute } from './create_rules_route'; +import { + getFindResult, + getResult, + createActionResult, + addPrepackagedRulesRequest, + getFindResultWithSingleHit, +} from '../__mocks__/request_responses'; + +jest.mock('../../rules/get_prepackaged_rules', () => { + return { + getPrepackagedRules: () => { + return [ + { + 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: 'severity', + interval: '5m', + type: 'query', + version: 2, // set one higher than the mocks which is set to 1 to trigger updates + }, + ]; + }, + }; +}); + +import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; + +describe('add_prepackaged_rules_route', () => { + let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); + elasticsearch.getCluster = getMockNonEmptyIndex(); + + addPrepackedRulesRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when creating a with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(addPrepackagedRulesRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + createRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(addPrepackagedRulesRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + createRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(addPrepackagedRulesRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + createRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject( + addPrepackagedRulesRequest() + ); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('it returns a 400 if the index does not exist', async () => { + elasticsearch.getCluster = getMockEmptyIndex(); + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + 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, + }); + }); + }); + + describe('payload', () => { + test('1 rule is installed and 0 are updated when find results are empty', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(addPrepackagedRulesRequest()); + expect(JSON.parse(payload)).toEqual({ + rules_installed: 1, + rules_updated: 0, + }); + }); + + test('1 rule is updated and 0 are installed when we return a single find and the versions are different', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(addPrepackagedRulesRequest()); + expect(JSON.parse(payload)).toEqual({ + rules_installed: 0, + rules_updated: 1, + }); + }); + }); +}); 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 922b70e87467e..5ceecdb058e5f 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 @@ -45,15 +45,15 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR const callWithRequest = callWithRequestFactory(request, server); const rulesFromFileSystem = getPrepackagedRules(); - const prepackedRules = await getExistingPrepackagedRules({ alertsClient }); - const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackedRules); - const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackedRules); + const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); + const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); + const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); const spaceIndex = getIndex(request, server); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex); if (!spaceIndexExists) { - throw new Boom( + return Boom.badRequest( `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}` ); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index d5650b078e678..0931e941f8e46 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -9,10 +9,10 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, + getMockEmptyIndex, } from '../__mocks__/_mock_server'; import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, getResult, @@ -29,11 +29,7 @@ describe('create_rules_bulk', () => { beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => true), - })); - - createRulesBulkRoute((server as unknown) as ServerFacade); + createRulesBulkRoute(server); }); describe('status codes with actionClient and alertClient', () => { @@ -48,14 +44,14 @@ describe('create_rules_bulk', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + createRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadBulkRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + createRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadBulkRequest()); expect(statusCode).toBe(404); }); @@ -64,13 +60,32 @@ describe('create_rules_bulk', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - createRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + createRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { + test('it gets a 409 if the index does not exist', async () => { + elasticsearch.getCluster = getMockEmptyIndex(); + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(getReadBulkRequest()); + expect(JSON.parse(payload)).toEqual([ + { + error: { + message: + 'To create a rule, the index must exist first. Index .siem-signals does not exist', + status_code: 400, + }, + rule_id: 'rule-1', + }, + ]); + }); + test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 15e3361986ab9..9c18f9040008c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -87,7 +87,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou if (!indexExists) { return createBulkErrorObject({ ruleId: ruleIdOrUuid, - statusCode: 409, + statusCode: 400, message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, }); } 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 466d150d58466..77c6f6f3b4840 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 @@ -9,10 +9,11 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, + getMockNonEmptyIndex, + getMockEmptyIndex, } from '../__mocks__/_mock_server'; import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, @@ -42,13 +43,8 @@ describe('create_rules', () => { elasticsearch, savedObjectsClient, } = createMockServer()); - elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ - callWithRequest: jest - .fn() - .mockImplementation((endpoint, params) => ({ _shards: { total: 1 } })), - })); - - createRulesRoute((server as unknown) as ServerFacade); + elasticsearch.getCluster = getMockNonEmptyIndex(); + createRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { @@ -64,14 +60,14 @@ describe('create_rules', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + createRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + createRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); @@ -80,13 +76,27 @@ describe('create_rules', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - createRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + createRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { + test('it returns a 400 if the index does not exist', async () => { + elasticsearch.getCluster = getMockEmptyIndex(); + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + 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, + }); + }); + test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); 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 3e173e358557a..aa535d325f4b9 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 @@ -78,17 +78,14 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const callWithRequest = callWithRequestFactory(request, server); const indexExists = await getIndexExists(callWithRequest, finalIndex); if (!indexExists) { - return new Boom( - `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, - { - statusCode: 400, - } + return Boom.badRequest( + `To create a rule, the index must exist first. Index ${finalIndex} does not exist` ); } if (ruleId != null) { const rule = await readRules({ alertsClient, ruleId }); if (rule != null) { - return new Boom(`rule_id: "${ruleId}" already exists`, { statusCode: 409 }); + return Boom.conflict(`rule_id: "${ruleId}" already exists`); } } const createdRule = await createRules({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 3c429c3e38f3a..7b8496b2fe725 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -22,7 +22,6 @@ import { getDeleteAsPostBulkRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; -import { ServerFacade } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; @@ -33,7 +32,7 @@ describe('delete_rules', () => { beforeEach(() => { ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesBulkRoute((server as unknown) as ServerFacade); + deleteRulesBulkRoute(server); }); afterEach(() => { @@ -100,14 +99,14 @@ describe('delete_rules', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteRulesBulkRoute((serverWithoutActionClient as unknown) as ServerFacade); + deleteRulesBulkRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteBulkRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesBulkRoute((serverWithoutAlertClient as unknown) as ServerFacade); + deleteRulesBulkRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteBulkRequest()); expect(statusCode).toBe(404); }); @@ -116,7 +115,7 @@ describe('delete_rules', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteRulesBulkRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + deleteRulesBulkRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteBulkRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index ee4edada52b6a..2854312246c5f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -13,7 +13,6 @@ import { import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, @@ -30,7 +29,7 @@ describe('delete_rules', () => { beforeEach(() => { ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesRoute((server as unknown) as ServerFacade); + deleteRulesRoute(server); }); afterEach(() => { @@ -70,14 +69,14 @@ describe('delete_rules', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + deleteRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + deleteRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); @@ -86,7 +85,7 @@ describe('delete_rules', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + deleteRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 639e4877f7f72..0aab02281a536 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -13,7 +13,6 @@ import { import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -23,7 +22,7 @@ describe('find_rules', () => { beforeEach(() => { ({ server, alertsClient, actionsClient } = createMockServer()); - findRulesRoute((server as unknown) as ServerFacade); + findRulesRoute(server); }); afterEach(() => { @@ -46,14 +45,14 @@ describe('find_rules', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - findRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + findRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + findRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); @@ -62,7 +61,7 @@ describe('find_rules', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - findRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + findRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); 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_rule_status_route.test.ts new file mode 100644 index 0000000000000..1ae9e87b8eefe --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts @@ -0,0 +1,125 @@ +/* + * 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, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, + getMockNonEmptyIndex, +} from '../__mocks__/_mock_server'; +import { createRulesRoute } from './create_rules_route'; +import { + getFindResult, + getResult, + createActionResult, + getFindResultWithSingleHit, + getPrepackagedRulesStatusRequest, +} from '../__mocks__/request_responses'; + +jest.mock('../../rules/get_prepackaged_rules', () => { + return { + getPrepackagedRules: () => { + return [ + { + 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: 'severity', + interval: '5m', + type: 'query', + version: 2, // set one higher than the mocks which is set to 1 to trigger updates + }, + ]; + }, + }; +}); + +import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; + +describe('get_prepackaged_rule_status_route', () => { + let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); + elasticsearch.getCluster = getMockNonEmptyIndex(); + getPrepackagedRulesStatusRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when creating a with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getPrepackagedRulesStatusRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + createRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject( + getPrepackagedRulesStatusRequest() + ); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + createRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject( + getPrepackagedRulesStatusRequest() + ); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + createRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject( + getPrepackagedRulesStatusRequest() + ); + expect(statusCode).toBe(404); + }); + }); + + describe('payload', () => { + test('0 rules installed, 1 rules not installed, and 1 rule not updated', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); + expect(JSON.parse(payload)).toEqual({ + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + }); + }); + + test('1 rule installed, 0 rules not installed, and 1 rule to not updated', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); + expect(JSON.parse(payload)).toEqual({ + rules_installed: 1, + rules_not_installed: 0, + rules_not_updated: 1, + }); + }); + }); +}); 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 new file mode 100644 index 0000000000000..99e29242bced0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -0,0 +1,59 @@ +/* + * 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_PREPACKAGED_URL } from '../../../../../common/constants'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError } from '../utils'; +import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; +import { getRulesToInstall } from '../../rules/get_rules_to_install'; +import { getRulesToUpdate } from '../../rules/get_rules_to_update'; +import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; + +export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { + return { + method: 'GET', + path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + try { + const rulesFromFileSystem = getPrepackagedRules(); + const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); + const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); + const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); + return { + rules_installed: prepackagedRules.length, + rules_not_installed: rulesToInstall.length, + rules_not_updated: rulesToUpdate.length, + }; + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const getPrepackagedRulesStatusRoute = (server: ServerFacade): void => { + server.route(createGetPrepackagedRulesStatusRoute()); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 92db4be1c9ff9..4190225bea1f1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -13,7 +13,6 @@ import { import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, @@ -29,7 +28,7 @@ describe('read_signals', () => { beforeEach(() => { ({ server, alertsClient, savedObjectsClient } = createMockServer()); - readRulesRoute((server as unknown) as ServerFacade); + readRulesRoute(server); }); afterEach(() => { @@ -47,14 +46,14 @@ describe('read_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - readRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + readRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + readRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); @@ -63,7 +62,7 @@ describe('read_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - readRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + readRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 2ab2610834195..cc41800671d7d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -13,7 +13,6 @@ import { import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, @@ -33,7 +32,7 @@ describe('update_rules_bulk', () => { beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - updateRulesBulkRoute((server as unknown) as ServerFacade); + updateRulesBulkRoute(server); }); describe('status codes with actionClient and alertClient', () => { @@ -73,14 +72,14 @@ describe('update_rules_bulk', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + updateRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + updateRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); @@ -89,7 +88,7 @@ describe('update_rules_bulk', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - updateRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + updateRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index d33989b8a7ce5..a7e8f1b1c0a7e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -13,7 +13,6 @@ import { import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getFindResult, @@ -32,7 +31,7 @@ describe('update_rules', () => { beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - updateRulesRoute((server as unknown) as ServerFacade); + updateRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { @@ -58,14 +57,14 @@ describe('update_rules', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateRulesRoute((serverWithoutActionClient as unknown) as ServerFacade); + updateRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute((serverWithoutAlertClient as unknown) as ServerFacade); + updateRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); @@ -74,7 +73,7 @@ describe('update_rules', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - updateRulesRoute((serverWithoutActionOrAlertClient as unknown) as ServerFacade); + updateRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); 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 7d4fb43b58ef1..ae79b571b2b62 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 @@ -34,11 +34,11 @@ export const getIdError = ({ ruleId: string | undefined | null; }) => { if (id != null) { - return new Boom(`id: "${id}" not found`, { statusCode: 404 }); + return Boom.notFound(`id: "${id}" not found`); } else if (ruleId != null) { - return new Boom(`rule_id: "${ruleId}" not found`, { statusCode: 404 }); + return Boom.notFound(`rule_id: "${ruleId}" not found`); } else { - return new Boom(`id or rule_id should have been defined`, { statusCode: 404 }); + return Boom.notFound('id or rule_id should have been defined'); } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 74eb4d6c8e918..1993948808ef4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -205,8 +205,8 @@ describe('add prepackaged rules schema', () => { query: 'some query', language: 'kuery', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('"output_index" is not allowed'); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version] does validate', () => { @@ -345,6 +345,48 @@ describe('add prepackaged rules schema', () => { ).toEqual(true); }); + test('immutable cannot be false', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + immutable: false, + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + version: 1, + }).error.message + ).toEqual('child "immutable" fails because ["immutable" must be one of [true]]'); + }); + + test('immutable can be true', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + immutable: true, + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + version: 1, + }).error + ).toBeFalsy(); + }); + test('defaults enabled to false', () => { expect( addPrepackagedRulesSchema.validate>({ @@ -380,8 +422,8 @@ describe('add prepackaged rules schema', () => { query: 'some-query', language: 'kuery', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "rule_id" fails because ["rule_id" is required]'); }); test('references cannot be numbers', () => { @@ -403,8 +445,10 @@ describe('add prepackaged rules schema', () => { language: 'kuery', references: [5], version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); }); test('indexes cannot be numbers', () => { @@ -425,8 +469,10 @@ describe('add prepackaged rules schema', () => { query: 'some-query', language: 'kuery', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); }); test('defaults interval to 5 min', () => { @@ -478,8 +524,8 @@ describe('add prepackaged rules schema', () => { interval: '5m', type: 'saved_query', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "saved_id" fails because ["saved_id" is required]'); }); test('saved_id is required when type is saved_query and validates with it', () => { @@ -539,8 +585,8 @@ describe('add prepackaged rules schema', () => { saved_id: 'some id', filters: 'some string', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); }); test('language validates with kuery', () => { @@ -602,8 +648,8 @@ describe('add prepackaged rules schema', () => { query: 'some query', language: 'something-made-up', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); }); test('max_signals cannot be negative', () => { @@ -624,8 +670,8 @@ describe('add prepackaged rules schema', () => { language: 'kuery', max_signals: -1, version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals cannot be zero', () => { @@ -646,8 +692,8 @@ describe('add prepackaged rules schema', () => { language: 'kuery', max_signals: 0, version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals can be 1', () => { @@ -716,8 +762,10 @@ describe('add prepackaged rules schema', () => { max_signals: 1, tags: [0, 1, 2], version: 1, - }).error - ).toBeTruthy(); + }).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 threats that are missing "framework"', () => { @@ -758,9 +806,12 @@ describe('add prepackaged rules schema', () => { }, ], version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); }); + test('You cannot send in an array of threats that are missing "tactic"', () => { expect( addPrepackagedRulesSchema.validate< @@ -795,9 +846,12 @@ describe('add prepackaged rules schema', () => { }, ], version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + ); }); + test('You cannot send in an array of threats that are missing "techniques"', () => { expect( addPrepackagedRulesSchema.validate< @@ -830,8 +884,10 @@ describe('add prepackaged rules schema', () => { }, ], version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]' + ); }); test('You can optionally send in an array of false positives', () => { @@ -878,8 +934,10 @@ describe('add prepackaged rules schema', () => { language: 'kuery', max_signals: 1, version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' + ); }); test('You can optionally set the immutable to be true', () => { @@ -926,8 +984,8 @@ describe('add prepackaged rules schema', () => { language: 'kuery', max_signals: 1, version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "immutable" fails because ["immutable" must be a boolean]'); }); test('You cannot set the risk_score to 101', () => { @@ -949,8 +1007,8 @@ describe('add prepackaged rules schema', () => { language: 'kuery', max_signals: 1, version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); }); test('You cannot set the risk_score to -1', () => { @@ -972,8 +1030,8 @@ describe('add prepackaged rules schema', () => { language: 'kuery', max_signals: 1, version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); }); test('You can set the risk_score to 0', () => { @@ -1070,8 +1128,8 @@ describe('add prepackaged rules schema', () => { max_signals: 1, meta: 'should not work', version: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); }); test('You can omit the query string when filters are present', () => { @@ -1140,8 +1198,8 @@ describe('add prepackaged rules schema', () => { max_signals: 1, version: 1, timeline_id: 'timeline-id', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1165,8 +1223,8 @@ describe('add prepackaged rules schema', () => { version: 1, timeline_id: 'timeline-id', timeline_title: null, - }).error - ).toBeTruthy(); + }).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', () => { @@ -1190,8 +1248,8 @@ describe('add prepackaged rules schema', () => { version: 1, timeline_id: 'timeline-id', timeline_title: '', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1215,8 +1273,8 @@ describe('add prepackaged rules schema', () => { version: 1, timeline_id: '', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1239,7 +1297,7 @@ describe('add prepackaged rules schema', () => { max_signals: 1, version: 1, timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index 49907b4a975e6..9311371d630f7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -42,7 +42,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * Big differences between this schema and the createRulesSchema * - rule_id is required here * - output_index is not allowed (and instead the space index must be used) - * - immutable defaults to true instead of to false + * - immutable defaults to true instead of to false and if it is there can only be true * - enabled defaults to false instead of true * - version is a required field that must exist */ @@ -53,7 +53,7 @@ export const addPrepackagedRulesSchema = Joi.object({ filters, from: from.required(), rule_id: rule_id.required(), - immutable: immutable.default(true), + immutable: immutable.default(true).valid(true), index, interval: interval.default('5m'), query: query.allow('').default(''), 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 87916bea60649..15f4fa7f05648 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 @@ -238,6 +238,7 @@ describe('create rules schema', () => { }).error ).toBeFalsy(); }); + test('You can send in an empty array to threats', () => { expect( createRulesSchema.validate>({ @@ -260,6 +261,7 @@ describe('create rules schema', () => { }).error ).toBeFalsy(); }); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { expect( createRulesSchema.validate>({ @@ -355,8 +357,10 @@ describe('create rules schema', () => { query: 'some-query', language: 'kuery', references: [5], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); }); test('indexes cannot be numbers', () => { @@ -377,8 +381,10 @@ describe('create rules schema', () => { query: 'some-query', language: 'kuery', } - ).error - ).toBeTruthy(); + ).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); }); test('defaults interval to 5 min', () => { @@ -430,8 +436,8 @@ describe('create rules schema', () => { severity: 'severity', interval: '5m', type: 'saved_query', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "saved_id" fails because ["saved_id" is required]'); }); test('saved_id is required when type is saved_query and validates with it', () => { @@ -491,8 +497,8 @@ describe('create rules schema', () => { type: 'saved_query', saved_id: 'some id', filters: 'some string', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); }); test('language validates with kuery', () => { @@ -554,8 +560,8 @@ describe('create rules schema', () => { references: ['index-1'], query: 'some query', language: 'something-made-up', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); }); test('max_signals cannot be negative', () => { @@ -576,8 +582,8 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', max_signals: -1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals cannot be zero', () => { @@ -598,8 +604,8 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', max_signals: 0, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals can be 1', () => { @@ -666,8 +672,10 @@ describe('create rules schema', () => { language: 'kuery', max_signals: 1, tags: [0, 1, 2], - }).error - ).toBeTruthy(); + }).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 threats that are missing "framework"', () => { @@ -708,9 +716,12 @@ describe('create rules schema', () => { ], }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); }); + test('You cannot send in an array of threats that are missing "tactic"', () => { expect( createRulesSchema.validate< @@ -745,9 +756,12 @@ describe('create rules schema', () => { ], }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + ); }); + test('You cannot send in an array of threats that are missing "techniques"', () => { expect( createRulesSchema.validate< @@ -780,8 +794,10 @@ describe('create rules schema', () => { }, }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]' + ); }); test('You can optionally send in an array of false positives', () => { @@ -828,34 +844,13 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' + ); }); - test('You can optionally set the immutable to be true', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot set the immutable to be a number', () => { + test('You cannot set the immutable when trying to create a rule', () => { expect( createRulesSchema.validate< Partial> & { immutable: number } @@ -876,8 +871,8 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('"immutable" is not allowed'); }); test('You cannot set the risk_score to 101', () => { @@ -899,8 +894,8 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); }); test('You cannot set the risk_score to -1', () => { @@ -922,8 +917,8 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); }); test('You can set the risk_score to 0', () => { @@ -935,7 +930,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -958,7 +952,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -981,7 +974,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -1018,8 +1010,8 @@ describe('create rules schema', () => { language: 'kuery', max_signals: 1, meta: 'should not work', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); }); test('You can omit the query string when filters are present', () => { @@ -1031,7 +1023,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -1086,8 +1077,8 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', timeline_id: 'some_id', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1109,8 +1100,8 @@ describe('create rules schema', () => { language: 'kuery', timeline_id: 'some_id', timeline_title: null, - }).error - ).toBeTruthy(); + }).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', () => { @@ -1132,8 +1123,8 @@ describe('create rules schema', () => { language: 'kuery', timeline_id: 'some_id', timeline_title: '', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1155,8 +1146,8 @@ describe('create rules schema', () => { language: 'kuery', timeline_id: '', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1177,7 +1168,7 @@ describe('create rules schema', () => { query: 'some query', language: 'kuery', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index df5c1694d6c78..5d9972453fb1a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -13,7 +13,6 @@ import { false_positives, filters, from, - immutable, index, rule_id, interval, @@ -46,7 +45,6 @@ export const createRulesSchema = Joi.object({ filters, from: from.required(), rule_id, - immutable: immutable.default(false), index, interval: interval.default('5m'), query: query.allow('').default(''), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts index 7850e3a733f09..dd3adf53f503b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts @@ -37,8 +37,10 @@ describe('create rules schema', () => { expect( exportRulesSchema.validate>({ objects: [{ id: 'test-1' }], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "objects" fails because ["objects" at position 0 fails because ["id" is not allowed]]' + ); }); }); @@ -70,8 +72,8 @@ describe('create rules schema', () => { Partial & { file_name: number }> >({ file_name: 5, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "file_name" fails because ["file_name" must be a string]'); }); test('exclude_export_details validates with a boolean true', () => { @@ -92,8 +94,10 @@ describe('create rules schema', () => { > >({ exclude_export_details: 'blah', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "exclude_export_details" fails because ["exclude_export_details" must be a boolean]' + ); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts index 14b3bdb298739..339874e19c33a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts @@ -69,8 +69,10 @@ describe('find rules schema', () => { expect( findRulesSchema.validate> & { fields: number[] }>({ fields: [5], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "fields" fails because ["fields" at position 0 fails because ["0" must be a string]]' + ); }); test('per page has a default of 20', () => { @@ -93,16 +95,16 @@ describe('find rules schema', () => { expect( findRulesSchema.validate> & { filter: number }>({ filter: 5, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "filter" fails because ["filter" must be a string]'); }); test('sort_order requires sort_field to work', () => { expect( findRulesSchema.validate>({ sort_order: 'asc', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "sort_field" fails because ["sort_field" is required]'); }); test('sort_order and sort_field validate together', () => { @@ -130,7 +132,7 @@ describe('find rules schema', () => { >({ sort_order: 'some other string', sort_field: 'some field', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "sort_order" fails because ["sort_order" must be one of [asc, desc]]'); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index 09bc7a70711ec..bed64cc6e7a02 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -243,6 +243,7 @@ describe('import rules schema', () => { }).error ).toBeFalsy(); }); + test('You can send in an empty array to threats', () => { expect( importRulesSchema.validate>({ @@ -265,6 +266,7 @@ describe('import rules schema', () => { }).error ).toBeFalsy(); }); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { expect( importRulesSchema.validate>({ @@ -360,8 +362,10 @@ describe('import rules schema', () => { query: 'some-query', language: 'kuery', references: [5], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); }); test('indexes cannot be numbers', () => { @@ -382,8 +386,10 @@ describe('import rules schema', () => { type: 'query', query: 'some-query', language: 'kuery', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' + ); }); test('defaults interval to 5 min', () => { @@ -435,8 +441,8 @@ describe('import rules schema', () => { severity: 'severity', interval: '5m', type: 'saved_query', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "saved_id" fails because ["saved_id" is required]'); }); test('saved_id is required when type is saved_query and validates with it', () => { @@ -496,8 +502,8 @@ describe('import rules schema', () => { type: 'saved_query', saved_id: 'some id', filters: 'some string', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); }); test('language validates with kuery', () => { @@ -559,8 +565,8 @@ describe('import rules schema', () => { references: ['index-1'], query: 'some query', language: 'something-made-up', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); }); test('max_signals cannot be negative', () => { @@ -581,8 +587,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', max_signals: -1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals cannot be zero', () => { @@ -603,8 +609,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', max_signals: 0, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals can be 1', () => { @@ -673,8 +679,10 @@ describe('import rules schema', () => { max_signals: 1, tags: [0, 1, 2], } - ).error - ).toBeTruthy(); + ).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 threats that are missing "framework"', () => { @@ -715,9 +723,12 @@ describe('import rules schema', () => { ], }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); }); + test('You cannot send in an array of threats that are missing "tactic"', () => { expect( importRulesSchema.validate< @@ -752,9 +763,12 @@ describe('import rules schema', () => { ], }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + ); }); + test('You cannot send in an array of threats that are missing "techniques"', () => { expect( importRulesSchema.validate< @@ -787,8 +801,10 @@ describe('import rules schema', () => { }, }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]' + ); }); test('You can optionally send in an array of false positives', () => { @@ -835,8 +851,10 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' + ); }); test('You can optionally set the immutable to be true', () => { @@ -883,8 +901,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "immutable" fails because ["immutable" must be a boolean]'); }); test('You cannot set the risk_score to 101', () => { @@ -906,8 +924,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); }); test('You cannot set the risk_score to -1', () => { @@ -929,8 +947,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', max_signals: 1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); }); test('You can set the risk_score to 0', () => { @@ -1025,8 +1043,8 @@ describe('import rules schema', () => { language: 'kuery', max_signals: 1, meta: 'should not work', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); }); test('You can omit the query string when filters are present', () => { @@ -1093,8 +1111,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', timeline_id: 'some_id', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1116,8 +1134,8 @@ describe('import rules schema', () => { language: 'kuery', timeline_id: 'some_id', timeline_title: null, - }).error - ).toBeTruthy(); + }).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', () => { @@ -1139,8 +1157,10 @@ describe('import rules schema', () => { language: 'kuery', timeline_id: 'some_id', timeline_title: '', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1162,8 +1182,8 @@ describe('import rules schema', () => { language: 'kuery', timeline_id: '', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).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', () => { @@ -1184,8 +1204,8 @@ describe('import rules schema', () => { query: 'some query', language: 'kuery', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); }); test('rule_id is required and you cannot get by with just id', () => { @@ -1205,8 +1225,8 @@ describe('import rules schema', () => { references: ['index-1'], query: 'some query', language: 'kuery', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "rule_id" fails because ["rule_id" is required]'); }); test('it validates with created_at, updated_at, created_by, updated_by values', () => { @@ -1255,8 +1275,8 @@ describe('import rules schema', () => { updated_at: '2020-01-09T06:15:24.749Z', created_by: 'Braden Hassanabad', updated_by: 'Evan Hassanabad', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "created_at" fails because ["created_at" must be a valid ISO 8601 date]'); }); test('it does not validate with epoch strings for updated_at', () => { @@ -1280,8 +1300,8 @@ describe('import rules schema', () => { updated_at: '1578550728650', created_by: 'Braden Hassanabad', updated_by: 'Evan Hassanabad', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "updated_at" fails because ["updated_at" must be a valid ISO 8601 date]'); }); }); 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 6450da37699d8..ab1ffaab49165 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 @@ -24,8 +24,10 @@ describe('query_rules_bulk_schema', () => { rule_id: '1', id: '1', }, - ]).error - ).toBeTruthy(); + ]).error.message + ).toEqual( + '"value" at position 0 fails because ["value" contains a conflict between exclusive peers [id, rule_id]]' + ); }); test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => { @@ -38,8 +40,10 @@ describe('query_rules_bulk_schema', () => { rule_id: '1', id: '1', }, - ]).error - ).toBeTruthy(); + ]).error.message + ).toEqual( + '"value" at position 1 fails because ["value" contains a conflict between exclusive peers [id, rule_id]]' + ); }); test('only id validates', () => { 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 6c4e96abd2b98..c89d60e773a77 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 @@ -12,10 +12,11 @@ describe('queryRulesSchema', () => { expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); - test('both rule_id and id being supplied dot not validate', () => { + test('both rule_id and id being supplied do not validate', () => { expect( queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error - ).toBeTruthy(); + .message + ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); }); test('only id validates', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts index 792c7afad05b1..a6ba9b19a9d7d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts @@ -30,24 +30,24 @@ describe('set signal status schema', () => { expect( setSignalsStatusSchema.validate>({ signal_ids: ['somefakeid'], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "status" fails because ["status" is required]'); }); test('query and missing status is invalid', () => { expect( setSignalsStatusSchema.validate>({ query: {}, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "status" fails because ["status" is required]'); }); test('status is present but query or signal_ids is missing is invalid', () => { expect( setSignalsStatusSchema.validate>({ status: 'closed', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('"value" must contain at least one of [signal_ids, query]'); }); test('signal_ids is present but status has wrong value', () => { @@ -60,7 +60,7 @@ describe('set signal status schema', () => { > >({ status: 'fakeVal', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "status" fails because ["status" must be one of [open, closed]]'); }); }); 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 f713840ab43f9..823ebb90a3b3c 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 @@ -442,8 +442,10 @@ describe('update rules schema', () => { query: 'some-query', language: 'kuery', references: [5], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' + ); }); test('indexes cannot be numbers', () => { @@ -462,8 +464,10 @@ describe('update rules schema', () => { type: 'query', query: 'some-query', language: 'kuery', - }).error - ).toBeTruthy(); + }).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', () => { @@ -570,8 +574,8 @@ describe('update rules schema', () => { references: ['index-1'], query: 'some query', language: 'something-made-up', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); }); test('max_signals cannot be negative', () => { @@ -590,8 +594,8 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: -1, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals cannot be zero', () => { @@ -610,8 +614,8 @@ describe('update rules schema', () => { query: 'some query', language: 'kuery', max_signals: 0, - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); }); test('max_signals can be 1', () => { @@ -643,15 +647,15 @@ describe('update rules schema', () => { ).toBeFalsy(); }); - test('You update meta as a string', () => { + test('You cannot update meta as a string', () => { expect( updateRulesSchema.validate< Partial & { meta: string }> >({ id: 'rule-1', meta: 'should not work', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "meta" fails because ["meta" must be an object]'); }); test('filters cannot be a string', () => { @@ -662,8 +666,8 @@ describe('update rules schema', () => { rule_id: 'rule-1', type: 'query', filters: 'some string', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "filters" fails because ["filters" must be an array]'); }); test('threats is not defaulted to empty array on update', () => { @@ -706,6 +710,7 @@ describe('update rules schema', () => { }).value.threats ).toMatchObject([]); }); + test('threats is valid when updated with all sub-objects', () => { const expected: ThreatParams[] = [ { @@ -759,6 +764,7 @@ describe('update rules schema', () => { }).value.threats ).toMatchObject(expected); }); + test('threats is invalid when updated with missing property framework', () => { expect( updateRulesSchema.validate< @@ -795,9 +801,12 @@ describe('update rules schema', () => { ], }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "framework" fails because ["framework" is required]]]' + ); }); + test('threats is invalid when updated with missing tactic sub-object', () => { expect( updateRulesSchema.validate< @@ -830,9 +839,12 @@ describe('update rules schema', () => { ], }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' + ); }); + test('threats is invalid when updated with missing techniques', () => { expect( updateRulesSchema.validate< @@ -863,8 +875,10 @@ describe('update rules schema', () => { }, }, ], - }).error - ).toBeTruthy(); + }).error.message + ).toEqual( + 'child "threats" fails because ["threats" at position 0 fails because [child "techniques" fails because ["techniques" is required]]]' + ); }); test('validates with timeline_id and timeline_title', () => { @@ -900,8 +914,8 @@ describe('update rules schema', () => { type: 'saved_query', saved_id: 'some id', timeline_id: 'some-id', - }).error - ).toBeTruthy(); + }).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', () => { @@ -919,8 +933,8 @@ describe('update rules schema', () => { saved_id: 'some id', timeline_id: 'timeline-id', timeline_title: null, - }).error - ).toBeTruthy(); + }).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', () => { @@ -938,8 +952,8 @@ describe('update rules schema', () => { saved_id: 'some id', timeline_id: 'some-id', timeline_title: '', - }).error - ).toBeTruthy(); + }).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', () => { @@ -957,8 +971,8 @@ describe('update rules schema', () => { saved_id: 'some id', timeline_id: '', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).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', () => { @@ -975,7 +989,7 @@ describe('update rules schema', () => { type: 'saved_query', saved_id: 'some id', timeline_title: 'some-title', - }).error - ).toBeTruthy(); + }).error.message + ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); }); }); 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 9c3188738faea..d363bfca98466 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 @@ -13,7 +13,6 @@ import { false_positives, filters, from, - immutable, index, rule_id, interval, @@ -46,7 +45,6 @@ export const updateRulesSchema = Joi.object({ from, rule_id, id, - immutable, index, interval, query: query.allow(''), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 2b7ee443880e5..35e1e5933af64 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -8,7 +8,6 @@ import { createMockServer } from '../__mocks__/_mock_server'; import { setSignalsStatusRoute } from './open_close_signals_route'; import * as myUtils from '../utils'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getSetSignalStatusByIdsRequest, @@ -29,7 +28,7 @@ describe('set signal status', () => { elasticsearch.getCluster = jest.fn(() => ({ callWithRequest: jest.fn(() => true), })); - setSignalsStatusRoute((server as unknown) as ServerFacade); + setSignalsStatusRoute(server); }); describe('status on signal', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index aef499675b884..5b86d0a4b36c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -8,7 +8,6 @@ import { createMockServer } from '../__mocks__/_mock_server'; import { querySignalsRoute } from './query_signals_route'; import * as myUtils from '../utils'; import { ServerInjectOptions } from 'hapi'; -import { ServerFacade } from '../../../../types'; import { getSignalsQueryRequest, @@ -28,7 +27,7 @@ describe('query for signal', () => { elasticsearch.getCluster = jest.fn(() => ({ callWithRequest: jest.fn(() => true), })); - querySignalsRoute((server as unknown) as ServerFacade); + querySignalsRoute(server); }); describe('query and agg on signals index', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh new file mode 100755 index 0000000000000..40b10b15e21f6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_prepackaged_rules_status.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./get_prepackaged_rules_status.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/prepackaged/_status | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json index e9d955f920571..4a90d904f31ab 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json @@ -21,7 +21,6 @@ } ], "enabled": false, - "immutable": false, "index": ["auditbeat-*", "filebeat-*"], "interval": "5m", "query": "user.name: root or user.name: admin", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json index 16d5d6cc2b36a..2b7dbc8cccf0e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json @@ -21,7 +21,6 @@ } ], "enabled": false, - "immutable": true, "index": ["auditbeat-*", "filebeat-*"], "interval": "5m", "query": "user.name: root or user.name: admin", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_immutable.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_immutable.json deleted file mode 100644 index 14706b6e54e7b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_immutable.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rule_id": "query-rule-id", - "immutable": true -} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json index 7fc8de9fe8f9e..a47d0155727d8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json @@ -21,7 +21,6 @@ } ], "enabled": false, - "immutable": true, "index": ["auditbeat-*", "filebeat-*"], "interval": "5m", "query": "user.name: root or user.name: admin",