From f2ec57307289a2ca731563517e5ed20f6bd6f1d1 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Thu, 12 Nov 2020 21:22:13 -0500 Subject: [PATCH] [Security Solution][Detections] Adds framework for replacing API schemas (#82462) * Adds framework for replacing API schemas * Update integration tests with new schema * Fix response type on createRule helper * Add unit tests for new rule schema, add defaults for some array fields, clean up API schema definitions * Naming updates and linting fixes * Replace create_rules_bulk_schema and refactor route * Convert update_rules_route to new schema * Fix missing name error * Fix more tests * Fix import * Update patch route with internal schema validation * Reorganize new schema as drop-in replacement for create_rules_schema * Replace updateRulesSchema with new version * Cleanup - remove references to specific files within request folder * Fix imports * Fix tests * Allow a few more fields to be undefined in internal schema * Add static types back to test payloads, add more tests, add NonEmptyArray type builder * Pull defaults into reusable function --- .../schemas/common/schemas.ts | 39 +- .../request/create_rules_bulk_schema.test.ts | 100 +- .../request/create_rules_bulk_schema.ts | 4 +- .../request/create_rules_schema.mock.ts | 157 -- .../schemas/request/create_rules_schema.ts | 177 -- .../create_rules_type_dependents.test.ts | 81 +- .../request/create_rules_type_dependents.ts | 96 +- .../detection_engine/schemas/request/index.ts | 3 +- .../schemas/request/rule_schemas.mock.ts | 195 ++ ...es_schema.test.ts => rule_schemas.test.ts} | 763 ++------ .../schemas/request/rule_schemas.ts | 442 +++++ .../request/update_rules_bulk_schema.test.ts | 85 +- .../request/update_rules_bulk_schema.ts | 5 +- .../request/update_rules_schema.mock.ts | 45 - .../request/update_rules_schema.test.ts | 1628 ----------------- .../schemas/request/update_rules_schema.ts | 183 -- .../update_rules_type_dependents.test.ts | 43 +- .../request/update_rules_type_dependents.ts | 89 +- .../detection_engine/schemas/types/index.ts | 1 + .../schemas/types/non_empty_array.test.ts | 94 + .../schemas/types/non_empty_array.ts | 28 + .../schemas/types/threat_mapping.test.ts | 13 + .../schemas/types/threat_mapping.ts | 3 +- .../detection_engine/rules/api.test.ts | 8 +- .../containers/detection_engine/rules/api.ts | 5 +- .../rules/use_create_rule.test.tsx | 6 +- .../rules/use_update_rule.test.tsx | 2 +- .../rules/create/helpers.test.ts | 2 +- .../routes/__mocks__/request_responses.ts | 2 +- .../rules/create_rules_bulk_route.test.ts | 6 +- .../routes/rules/create_rules_bulk_route.ts | 173 +- .../routes/rules/create_rules_route.test.ts | 6 +- .../routes/rules/create_rules_route.ts | 175 +- .../rules/patch_rules_bulk_route.test.ts | 3 +- .../routes/rules/patch_rules_route.test.ts | 1 + .../rules/update_rules_bulk_route.test.ts | 6 +- .../routes/rules/update_rules_bulk_route.ts | 137 +- .../routes/rules/update_rules_route.test.ts | 8 +- .../routes/rules/update_rules_route.ts | 134 +- .../routes/rules/utils.test.ts | 8 +- .../detection_engine/routes/rules/utils.ts | 7 +- .../detection_engine/routes/rules/validate.ts | 17 + .../lib/detection_engine/rules/patch_rules.ts | 38 +- .../lib/detection_engine/rules/types.ts | 50 +- .../rules/update_rules.mock.ts | 102 +- .../rules/update_rules.test.ts | 45 +- .../detection_engine/rules/update_rules.ts | 214 +-- .../lib/detection_engine/rules/utils.ts | 9 +- .../schemas/rule_converters.ts | 266 +++ .../detection_engine/schemas/rule_schemas.ts | 210 +++ .../basic/tests/create_rules.ts | 45 +- .../basic/tests/update_rules.ts | 13 +- .../basic/tests/update_rules_bulk.ts | 21 +- .../security_and_spaces/tests/add_actions.ts | 2 +- .../tests/create_exceptions.ts | 22 +- .../security_and_spaces/tests/create_rules.ts | 45 +- .../tests/create_threat_matching.ts | 34 +- .../tests/generating_signals.ts | 32 +- .../security_and_spaces/tests/update_rules.ts | 11 +- .../tests/update_rules_bulk.ts | 21 +- .../detection_engine_api_integration/utils.ts | 22 +- 61 files changed, 1921 insertions(+), 4261 deletions(-) delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts rename x-pack/plugins/security_solution/common/detection_engine/schemas/request/{create_rules_schema.test.ts => rule_schemas.test.ts} (63%) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 2180ebacc9db7..82b803c62a940 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -9,6 +9,11 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; +import { + SavedObjectAttributes, + SavedObjectAttribute, + SavedObjectAttributeSingle, +} from 'src/core/types'; import { RiskScore } from '../types/risk_score'; import { UUID } from '../types/uuid'; import { IsoDateString } from '../types/iso_date_string'; @@ -66,6 +71,22 @@ export type ExcludeExportDetails = t.TypeOf; export const filters = t.array(t.unknown); // Filters are not easily type-able yet export type Filters = t.TypeOf; // Filters are not easily type-able yet +export const filtersOrUndefined = t.union([filters, t.undefined]); +export type FiltersOrUndefined = t.TypeOf; + +export const saved_object_attribute_single: t.Type = t.recursion( + 'saved_object_attribute_single', + () => t.union([t.string, t.number, t.boolean, t.null, t.undefined, saved_object_attributes]) +); +export const saved_object_attribute: t.Type = t.recursion( + 'saved_object_attribute', + () => t.union([saved_object_attribute_single, t.array(saved_object_attribute_single)]) +); +export const saved_object_attributes: t.Type = t.recursion( + 'saved_object_attributes', + () => t.record(t.string, saved_object_attribute) +); + /** * Params is an "object", since it is a type of AlertActionParams which is action templates. * @see x-pack/plugins/alerts/common/alert.ts @@ -73,7 +94,7 @@ export type Filters = t.TypeOf; // Filters are not easily type-a export const action_group = t.string; export const action_id = t.string; export const action_action_type_id = t.string; -export const action_params = t.object; +export const action_params = saved_object_attributes; export const action = t.exact( t.type({ group: action_group, @@ -86,6 +107,18 @@ export const action = t.exact( export const actions = t.array(action); export type Actions = t.TypeOf; +export const actionsCamel = t.array( + t.exact( + t.type({ + group: action_group, + id: action_id, + actionTypeId: action_action_type_id, + params: action_params, + }) + ) +); +export type ActionsCamel = t.TypeOf; + const stringValidator = (input: unknown): input is string => typeof input === 'string'; export const from = new t.Type( 'From', @@ -416,6 +449,10 @@ export const created_at = IsoDateString; export const updated_at = IsoDateString; export const updated_by = t.string; export const created_by = t.string; +export const updatedByOrNull = t.union([updated_by, t.null]); +export type UpdatedByOrNull = t.TypeOf; +export const createdByOrNull = t.union([created_by, t.null]); +export type CreatedByOrNull = t.TypeOf; export const version = PositiveIntegerGreaterThanZero; export type Version = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts index 5c086511e6760..0d70b67f137e8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts @@ -4,22 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createRulesBulkSchema, - CreateRulesBulkSchema, - CreateRulesBulkSchemaDecoded, -} from './create_rules_bulk_schema'; +import { createRulesBulkSchema, CreateRulesBulkSchema } from './create_rules_bulk_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight } from '../../../test_utils'; -import { - getCreateRulesSchemaMock, - getCreateRulesSchemaDecodedMock, -} from './create_rules_schema.mock'; import { formatErrors } from '../../../format_errors'; -import { CreateRulesSchema } from './create_rules_schema'; +import { getCreateRulesSchemaMock } from './rule_schemas.mock'; // only the basics of testing are here. -// see: create_rules_schema.test.ts for the bulk of the validation tests +// see: rule_schemas.test.ts for the bulk of the validation tests // this just wraps createRulesSchema in an array describe('create_rules_bulk_schema', () => { test('can take an empty array and validate it', () => { @@ -38,13 +30,16 @@ describe('create_rules_bulk_schema', () => { const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "description"' + ); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "risk_score"' + ); + expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"'); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "severity"' + ); expect(output.schema).toEqual({}); }); @@ -55,7 +50,7 @@ describe('create_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([getCreateRulesSchemaDecodedMock()]); + expect(output.schema).toEqual(payload); }); test('two array elements do validate', () => { @@ -65,10 +60,7 @@ describe('create_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - getCreateRulesSchemaDecodedMock(), - getCreateRulesSchemaDecodedMock(), - ]); + expect(output.schema).toEqual(payload); }); test('single array element with a missing value (risk_score) will not validate', () => { @@ -137,7 +129,7 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => { - const singleItem: CreateRulesSchema & { madeUpValue: string } = { + const singleItem = { ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; @@ -152,8 +144,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => { - const singleItem: CreateRulesSchema = getCreateRulesSchemaMock(); - const secondItem: CreateRulesSchema & { madeUpValue: string } = { + const singleItem = getCreateRulesSchemaMock(); + const secondItem = { ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; @@ -167,11 +159,11 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where both are invalid (extra key and value) will not validate', () => { - const singleItem: CreateRulesSchema & { madeUpValue: string } = { + const singleItem = { ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; - const secondItem: CreateRulesSchema & { madeUpValue: string } = { + const secondItem = { ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; @@ -184,28 +176,6 @@ describe('create_rules_bulk_schema', () => { expect(output.schema).toEqual({}); }); - test('The default for "from" will be "now-6m"', () => { - const { from, ...withoutFrom } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutFrom]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].from).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...withoutTo } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutTo]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].to).toEqual('now'); - }); - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { const badSeverity = { ...getCreateRulesSchemaMock(), severity: 'madeup' }; const payload = [badSeverity]; @@ -226,9 +196,7 @@ describe('create_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - { ...getCreateRulesSchemaDecodedMock(), note: '# test markdown' }, - ]); + expect(output.schema).toEqual(payload); }); test('You can set "note" to an empty string', () => { @@ -238,10 +206,10 @@ describe('create_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([{ ...getCreateRulesSchemaDecodedMock(), note: '' }]); + expect(output.schema).toEqual(payload); }); - test('You can set "note" to anything other than string', () => { + test('You cant set "note" to anything other than string', () => { const payload = [ { ...getCreateRulesSchemaMock(), @@ -259,26 +227,4 @@ describe('create_rules_bulk_schema', () => { ]); expect(output.schema).toEqual({}); }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...withoutActions } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutActions]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].actions).toEqual([]); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...withoutThrottle } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutThrottle]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].throttle).toEqual(null); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts index c6233cc63fa9f..81b8bf7abbf78 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts @@ -6,9 +6,7 @@ import * as t from 'io-ts'; -import { createRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema'; +import { createRulesSchema } from './rule_schemas'; export const createRulesBulkSchema = t.array(createRulesSchema); export type CreateRulesBulkSchema = t.TypeOf; - -export type CreateRulesBulkSchemaDecoded = CreateRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts deleted file mode 100644 index 94dd1215d8026..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CreateRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; - -export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({ - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - rule_id: ruleId, -}); - -export const getCreateMlRulesSchemaMock = (ruleId = 'rule-1') => { - const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(ruleId); - - return { - ...mlParams, - type: 'machine_learning', - anomaly_threshold: 58, - machine_learning_job_id: 'typical-ml-job-id', - }; -}; - -export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ - author: [], - severity_mapping: [], - risk_score_mapping: [], - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - from: 'now-6m', - interval: '5m', - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - to: 'now', - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - rule_id: 'rule-1', -}); - -export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({ - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: ruleId, - threat_query: '*:*', - threat_index: ['list-index'], - threat_mapping: [ - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [ - { - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - ], -}); - -export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ - author: [], - severity_mapping: [], - risk_score_mapping: [], - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'threat_match', - risk_score: 55, - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - from: 'now-6m', - interval: '5m', - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - to: 'now', - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - rule_id: 'rule-1', - threat_query: '*:*', - threat_index: ['list-index'], - threat_mapping: [ - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [ - { - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - ], -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts deleted file mode 100644 index 2fe52bbe470a5..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; - -import { - description, - anomaly_threshold, - building_block_type, - filters, - RuleId, - index, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - machine_learning_job_id, - risk_score, - MaxSignals, - name, - severity, - Tags, - To, - type, - Threat, - threshold, - ThrottleOrNull, - note, - Version, - References, - Actions, - Enabled, - FalsePositives, - From, - Interval, - language, - query, - license, - rule_name_override, - timestamp_override, - Author, - RiskScoreMapping, - SeverityMapping, - event_category_override, -} from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, -} from '../types/threat_mapping'; - -import { - DefaultStringArray, - DefaultActionsArray, - DefaultBooleanTrue, - DefaultFromString, - DefaultIntervalString, - DefaultMaxSignalsNumber, - DefaultToString, - DefaultThreatArray, - DefaultThrottleNull, - DefaultVersionNumber, - DefaultListArray, - ListArray, - DefaultUuid, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; - -export const createRulesSchema = t.intersection([ - t.exact( - t.type({ - description, - risk_score, - name, - severity, - type, - }) - ), - t.exact( - t.partial({ - actions: DefaultActionsArray, // defaults to empty actions array if not set during decode - anomaly_threshold, // defaults to undefined if not set during decode - author: DefaultStringArray, // defaults to empty array of strings if not set during decode - building_block_type, // defaults to undefined if not set during decode - enabled: DefaultBooleanTrue, // defaults to true if not set during decode - event_category_override, // defaults to "undefined" if not set during decode - false_positives: DefaultStringArray, // defaults to empty string array if not set during decode - filters, // defaults to undefined if not set during decode - from: DefaultFromString, // defaults to "now-6m" if not set during decode - rule_id: DefaultUuid, - index, // defaults to undefined if not set during decode - interval: DefaultIntervalString, // defaults to "5m" if not set during decode - query, // defaults to undefined if not set during decode - language, // defaults to undefined if not set during decode - license, // defaults to "undefined" if not set during decode - // TODO: output_index: This should be removed eventually - output_index, // defaults to "undefined" if not set during decode - saved_id, // defaults to "undefined" if not set during decode - timeline_id, // defaults to "undefined" if not set during decode - timeline_title, // defaults to "undefined" if not set during decode - meta, // defaults to "undefined" if not set during decode - machine_learning_job_id, // defaults to "undefined" if not set during decode - max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode - risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode - rule_name_override, // defaults to "undefined" if not set during decode - severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode - tags: DefaultStringArray, // defaults to empty string array if not set during decode - to: DefaultToString, // defaults to "now" if not set during decode - threat: DefaultThreatArray, // defaults to empty array if not set during decode - threshold, // defaults to "undefined" if not set during decode - throttle: DefaultThrottleNull, // defaults to "null" if not set during decode - timestamp_override, // defaults to "undefined" if not set during decode - references: DefaultStringArray, // defaults to empty array of strings if not set during decode - note, // defaults to "undefined" if not set during decode - version: DefaultVersionNumber, // defaults to 1 if not set during decode - exceptions_list: DefaultListArray, // defaults to empty array if not set during decode - threat_mapping, // defaults to "undefined" if not set during decode - threat_query, // defaults to "undefined" if not set during decode - threat_filters, // defaults to "undefined" if not set during decode - threat_index, // defaults to "undefined" if not set during decode - threat_language, // defaults "undefined" if not set during decode - concurrent_searches, // defaults "undefined" if not set during decode - items_per_search, // defaults "undefined" if not set during decode - }) - ), -]); - -export type CreateRulesSchema = t.TypeOf; - -// This type is used after a decode since some things are defaults after a decode. -export type CreateRulesSchemaDecoded = Omit< - CreateRulesSchema, - | 'author' - | 'references' - | 'actions' - | 'enabled' - | 'false_positives' - | 'from' - | 'interval' - | 'max_signals' - | 'risk_score_mapping' - | 'severity_mapping' - | 'tags' - | 'to' - | 'threat' - | 'throttle' - | 'version' - | 'exceptions_list' - | 'rule_id' -> & { - author: Author; - references: References; - actions: Actions; - enabled: Enabled; - false_positives: FalsePositives; - from: From; - interval: Interval; - max_signals: MaxSignals; - risk_score_mapping: RiskScoreMapping; - severity_mapping: SeverityMapping; - tags: Tags; - to: To; - threat: Threat; - throttle: ThrottleOrNull; - version: Version; - exceptions_list: ListArray; - rule_id: RuleId; -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index a78b41cd0da18..3c395df03e0f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -4,31 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getCreateRulesSchemaMock, - getCreateThreatMatchRulesSchemaMock, -} from './create_rules_schema.mock'; -import { CreateRulesSchema } from './create_rules_schema'; +import { getCreateRulesSchemaMock, getCreateThreatMatchRulesSchemaMock } from './rule_schemas.mock'; +import { CreateRulesSchema } from './rule_schemas'; import { createRuleValidateTypeDependents } from './create_rules_type_dependents'; describe('create_rules_type_dependents', () => { - test('saved_id is required when type is saved_query and will not validate without out', () => { - const schema: CreateRulesSchema = { ...getCreateRulesSchemaMock(), type: 'saved_query' }; - delete schema.saved_id; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "saved_query", "saved_id" is required']); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'saved_query', - saved_id: '123', - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual([]); - }); - test('You cannot omit timeline_title when timeline_id is present', () => { const schema: CreateRulesSchema = { ...getCreateRulesSchemaMock(), @@ -69,63 +49,6 @@ describe('create_rules_type_dependents', () => { expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); - test('threshold is required when type is threshold and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'threshold', - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); - }); - - test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'threshold', - threshold: { - field: '', - value: -1, - }, - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); - }); - - test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'threat_match', - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual([ - 'when "type" is "threat_match", "threat_index" is required', - 'when "type" is "threat_match", "threat_query" is required', - 'when "type" is "threat_match", "threat_mapping" is required', - ]); - }); - - test('validates with threat_index, threat_query, and threat_mapping when type is "threat_match"', () => { - const schema = getCreateThreatMatchRulesSchemaMock(); - const { threat_filters: threatFilters, ...noThreatFilters } = schema; - const errors = createRuleValidateTypeDependents(noThreatFilters); - expect(errors).toEqual([]); - }); - - test('does NOT validate when threat_mapping is an empty array', () => { - const schema: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), - threat_mapping: [], - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['threat_mapping" must have at least one element']); - }); - - test('validates with threat_index, threat_query, threat_mapping, and an optional threat_filters, when type is "threat_match"', () => { - const schema = getCreateThreatMatchRulesSchemaMock(); - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual([]); - }); - test('validates that both "items_per_search" and "concurrent_searches" works when together', () => { const schema: CreateRulesSchema = { ...getCreateThreatMatchRulesSchemaMock(), diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts index c93b0f0b14f6a..771540189ad5f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts @@ -4,69 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isMlRule } from '../../../machine_learning/helpers'; -import { isThreatMatchRule, isThresholdRule } from '../../utils'; -import { CreateRulesSchema } from './create_rules_schema'; - -export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.anomaly_threshold == null) { - return ['when "type" is "machine_learning" anomaly_threshold is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateQuery = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.query != null) { - return ['when "type" is "machine_learning", "query" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateLanguage = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.language != null) { - return ['when "type" is "machine_learning", "language" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateSavedId = (rule: CreateRulesSchema): string[] => { - if (rule.type === 'saved_query') { - if (rule.saved_id == null) { - return ['when "type" is "saved_query", "saved_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateMachineLearningJobId = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.machine_learning_job_id == null) { - return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; +import { CreateRulesSchema } from './rule_schemas'; export const validateTimelineId = (rule: CreateRulesSchema): string[] => { if (rule.timeline_id != null) { @@ -94,33 +32,9 @@ export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => { return []; }; -export const validateThreshold = (rule: CreateRulesSchema): string[] => { - if (isThresholdRule(rule.type)) { - if (!rule.threshold) { - return ['when "type" is "threshold", "threshold" is required']; - } else if (rule.threshold.value <= 0) { - return ['"threshold.value" has to be bigger than 0']; - } else { - return []; - } - } - return []; -}; - export const validateThreatMapping = (rule: CreateRulesSchema): string[] => { let errors: string[] = []; - if (isThreatMatchRule(rule.type)) { - if (rule.threat_mapping == null) { - errors = ['when "type" is "threat_match", "threat_mapping" is required', ...errors]; - } else if (rule.threat_mapping.length === 0) { - errors = ['threat_mapping" must have at least one element', ...errors]; - } - if (rule.threat_query == null) { - errors = ['when "type" is "threat_match", "threat_query" is required', ...errors]; - } - if (rule.threat_index == null) { - errors = ['when "type" is "threat_match", "threat_index" is required', ...errors]; - } + if (rule.type === 'threat_match') { if (rule.concurrent_searches == null && rule.items_per_search != null) { errors = ['when "items_per_search" exists, "concurrent_searches" must also exist', ...errors]; } @@ -133,14 +47,8 @@ export const validateThreatMapping = (rule: CreateRulesSchema): string[] => { export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => { return [ - ...validateAnomalyThreshold(schema), - ...validateQuery(schema), - ...validateLanguage(schema), - ...validateSavedId(schema), - ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), - ...validateThreshold(schema), ...validateThreatMapping(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts index abfbc39189643..a657191181c0f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts @@ -5,7 +5,6 @@ */ export * from './add_prepackaged_rules_schema'; export * from './create_rules_bulk_schema'; -export * from './create_rules_schema'; export * from './export_rules_schema'; export * from './find_rules_schema'; export * from './import_rules_schema'; @@ -15,4 +14,4 @@ export * from './query_rules_schema'; export * from './query_signals_index_schema'; export * from './set_signal_status_schema'; export * from './update_rules_bulk_schema'; -export * from './update_rules_schema'; +export * from './rule_schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts new file mode 100644 index 0000000000000..6be51d2a1adc2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -0,0 +1,195 @@ +/* + * 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 { + MachineLearningCreateSchema, + MachineLearningUpdateSchema, + QueryCreateSchema, + QueryUpdateSchema, + SavedQueryCreateSchema, + SavedQueryUpdateSchema, + ThreatMatchCreateSchema, + ThreatMatchUpdateSchema, + ThresholdCreateSchema, +} from './rule_schemas'; + +export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, +}); + +export const getCreateSavedQueryRulesSchemaMock = (ruleId = 'rule-1'): SavedQueryCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'saved_query', + saved_id: 'some id', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, +}); + +export const getCreateThreatMatchRulesSchemaMock = ( + ruleId = 'rule-1' +): ThreatMatchCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, + threat_query: '*:*', + threat_index: ['list-index'], + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); + +export const getCreateMachineLearningRulesSchemaMock = ( + ruleId = 'rule-1' +): MachineLearningCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + risk_score: 55, + rule_id: ruleId, + type: 'machine_learning', + anomaly_threshold: 58, + machine_learning_job_id: 'typical-ml-job-id', +}); + +export const getCreateThresholdRulesSchemaMock = (ruleId = 'rule-1'): ThresholdCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + risk_score: 55, + rule_id: ruleId, + type: 'threshold', + query: 'user.name: root or user.name: admin', + threshold: { + field: 'some.field', + value: 4, + }, +}); + +export const getUpdateRulesSchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): QueryUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + id, +}); + +export const getUpdateSavedQuerySchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): SavedQueryUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'saved_query', + saved_id: 'some id', + risk_score: 55, + language: 'kuery', + id, +}); + +export const getUpdateThreatMatchSchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): ThreatMatchUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + id, + threat_query: '*:*', + threat_index: ['list-index'], + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); + +export const getUpdateMachineLearningSchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): MachineLearningUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + risk_score: 55, + id, + type: 'machine_learning', + anomaly_threshold: 58, + machine_learning_job_id: 'typical-ml-job-id', +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts similarity index 63% rename from x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 1b6a8d6f27762..c9330bbe73c89 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -4,38 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createRulesSchema, - CreateRulesSchema, - CreateRulesSchemaDecoded, -} from './create_rules_schema'; +import { createRulesSchema, CreateRulesSchema, SavedQueryCreateSchema } from './rule_schemas'; import { exactCheck } from '../../../exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { - getCreateRulesSchemaMock, - getCreateRulesSchemaDecodedMock, + getCreateSavedQueryRulesSchemaMock, getCreateThreatMatchRulesSchemaMock, - getCreateThreatMatchRulesSchemaDecodedMock, -} from './create_rules_schema.mock'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + getCreateRulesSchemaMock, + getCreateThresholdRulesSchemaMock, +} from './rule_schemas.mock'; import { getListArrayMock } from '../types/lists.mock'; describe('create rules schema', () => { test('empty objects do not validate', () => { - const payload: Partial = {}; + const payload = {}; const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -60,13 +49,7 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -79,12 +62,7 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -98,12 +76,7 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -118,12 +91,7 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -139,11 +107,7 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -160,10 +124,7 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(message.errors.length).toBeGreaterThan(0); expect(message.schema).toEqual({}); }); @@ -249,33 +210,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { @@ -322,34 +257,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { @@ -373,35 +281,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { @@ -422,32 +302,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { @@ -472,33 +327,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You can send in an empty array to threat', () => { @@ -511,11 +340,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - threat: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { @@ -554,49 +379,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('allows references to be sent as valid', () => { @@ -609,24 +392,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - references: ['index-1'], - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults references to an array if it is not sent in', () => { - const { references, ...noReferences } = getCreateRulesSchemaMock(); - const decoded = createRulesSchema.decode(noReferences); - const checked = exactCheck(noReferences, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - references: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('references cannot be numbers', () => { @@ -656,8 +422,8 @@ describe('create rules schema', () => { }); test('saved_query type can have filters with it', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), + const payload: SavedQueryCreateSchema = { + ...getCreateSavedQueryRulesSchemaMock(), filters: [], }; @@ -665,15 +431,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - filters: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('filters cannot be a string', () => { - const payload: Omit & { filters: string } = { + const payload = { ...getCreateRulesSchemaMock(), filters: 'some string', }; @@ -697,11 +459,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - language: 'kuery', - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('language validates with lucene', () => { @@ -714,15 +472,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - language: 'lucene', - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('language does not validate with something made up', () => { - const payload: Omit & { language: string } = { + const payload = { ...getCreateRulesSchemaMock(), language: 'something-made-up', }; @@ -774,11 +528,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - max_signals: 1, - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You can optionally send in an array of tags', () => { @@ -791,15 +541,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - tags: ['tag_1', 'tag_2'], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You cannot send in an array of tags that are numbers', () => { - const payload: Omit & { tags: number[] } = { + const payload = { ...getCreateRulesSchemaMock(), tags: [0, 1, 2], }; @@ -816,9 +562,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of threat that are missing "framework"', () => { - const payload: Omit & { - threat: Array>>; - } = { + const payload = { ...getCreateRulesSchemaMock(), threat: [ { @@ -848,9 +592,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of threat that are missing "tactic"', () => { - const payload: Omit & { - threat: Array>>; - } = { + const payload = { ...getCreateRulesSchemaMock(), threat: [ { @@ -876,9 +618,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of threat that are missing "technique"', () => { - const payload: Omit & { - threat: Array>>; - } = { + const payload = { ...getCreateRulesSchemaMock(), threat: [ { @@ -911,15 +651,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - false_positives: ['false_1', 'false_2'], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You cannot send in an array of false positives that are numbers', () => { - const payload: Omit & { false_positives: number[] } = { + const payload = { ...getCreateRulesSchemaMock(), false_positives: [5, 4], }; @@ -935,7 +671,7 @@ describe('create rules schema', () => { }); test('You cannot set the immutable to a number when trying to create a rule', () => { - const payload: Omit & { immutable: number } = { + const payload = { ...getCreateRulesSchemaMock(), immutable: 5, }; @@ -985,11 +721,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - risk_score: 0, - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You can set the risk_score to 100', () => { @@ -1002,11 +734,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - risk_score: 100, - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You can set meta to any object you want', () => { @@ -1021,17 +749,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You cannot create meta as a string', () => { - const payload: Omit & { meta: string } = { + const payload = { ...getCreateRulesSchemaMock(), meta: 'should not work', }; @@ -1056,13 +778,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - - const { query: expectedQuery, ...expectedNoQuery } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoQuery, - filters: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('validates with timeline_id and timeline_title', () => { @@ -1076,16 +792,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const payload: Omit & { severity: string } = { + const payload = { ...getCreateRulesSchemaMock(), severity: 'junk', }; @@ -1098,7 +809,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of actions that are missing "group"', () => { - const payload: Omit = { + const payload = { ...getCreateRulesSchemaMock(), actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], }; @@ -1113,7 +824,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of actions that are missing "id"', () => { - const payload: Omit = { + const payload = { ...getCreateRulesSchemaMock(), actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], }; @@ -1128,7 +839,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of actions that are missing "action_type_id"', () => { - const payload: Omit = { + const payload = { ...getCreateRulesSchemaMock(), actions: [{ group: 'group', id: 'id', params: {} }], }; @@ -1143,7 +854,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of actions that are missing "params"', () => { - const payload: Omit = { + const payload = { ...getCreateRulesSchemaMock(), actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], }; @@ -1158,7 +869,7 @@ describe('create rules schema', () => { }); test('You cannot send in an array of actions that are including "actionTypeId"', () => { - const payload: Omit = { + const payload = { ...getCreateRulesSchemaMock(), actions: [ { @@ -1190,11 +901,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - note: '# documentation markdown here', - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You can set note to an empty string', () => { @@ -1207,15 +914,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - note: '', - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('You cannot create note as an object', () => { - const payload: Omit & { note: {} } = { + const payload = { ...getCreateRulesSchemaMock(), note: { somethingHere: 'something else', @@ -1278,154 +981,10 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); }); - test('defaults interval to 5 min', () => { - const { interval, ...noInterval } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noInterval, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { interval: expectedInterval, ...expectedNoInterval } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoInterval, - interval: '5m', - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults max signals to 100', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { max_signals, ...noMaxSignals } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noMaxSignals, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { - max_signals: expectedMaxSignals, - ...expectedNoMaxSignals - } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoMaxSignals, - max_signals: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "from" will be "now-6m"', () => { - const { from, ...noFrom } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noFrom, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { from: expectedFrom, ...expectedNoFrom } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoFrom, - from: 'now-6m', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...noTo } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noTo, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { to: expectedTo, ...expectedNoTo } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoTo, - to: 'now', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...noActions } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noActions, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { actions: expectedActions, ...expectedNoActions } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoActions, - actions: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...noThrottle } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noThrottle, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { throttle: expectedThrottle, ...expectedNoThrottle } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoThrottle, - throttle: null, - }; - expect(message.schema).toEqual(expected); - }); - test('machine_learning type does validate', () => { const payload: CreateRulesSchema = { type: 'machine_learning', @@ -1453,46 +1012,42 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - type: 'machine_learning', - anomaly_threshold: 50, - machine_learning_job_id: 'linux_anomalous_network_activity_ecs', - false_positives: [], - references: [], - risk_score: 50, - threat: [], - name: 'ss', - description: 'ss', - severity: 'low', - tags: [], - interval: '5m', - from: 'now-360s', - to: 'now', - meta: { from: '1m' }, - actions: [], - enabled: true, - throttle: 'no_actions', - exceptions_list: [], - max_signals: DEFAULT_MAX_SIGNALS, - version: 1, - rule_id: 'rule-1', - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); - test('it generates a uuid v4 whenever you omit the rule_id', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { rule_id, ...noRuleId } = getCreateRulesSchemaMock(); - const decoded = createRulesSchema.decode(noRuleId); - const checked = exactCheck(noRuleId, decoded); + test('saved_id is required when type is saved_query and will not validate without it', () => { + /* eslint-disable @typescript-eslint/naming-convention */ + const { saved_id, ...payload } = getCreateSavedQueryRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect((message.schema as CreateRulesSchemaDecoded).rule_id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "saved_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('threshold is required when type is threshold and will not validate without it', () => { + const { threshold, ...payload } = getCreateThresholdRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threshold"', + ]); + expect(message.schema).toEqual({}); + }); + + test('threshold rules fail validation if threshold is not greater than 0', () => { + const payload = getCreateThresholdRulesSchemaMock(); + payload.threshold.value = 0; + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "threshold,value"', + ]); + expect(message.schema).toEqual({}); }); describe('exception_list', () => { @@ -1517,47 +1072,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - filters: [], - exceptions_list: [ - { - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', - }, - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { @@ -1581,40 +1096,11 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - filters: [], - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - const payload: Omit & { - exceptions_list: Array<{ id: string; namespace_type: string }>; - } = { + const payload = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1661,34 +1147,7 @@ describe('create rules schema', () => { const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - filters: [], - }; - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); }); @@ -1698,27 +1157,39 @@ describe('create rules schema', () => { const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getCreateThreatMatchRulesSchemaDecodedMock(); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); + expect(message.schema).toEqual(payload); }); - test('You can set a threat query, index, mapping, filters, concurrent_searches, items_per_search with a when creating a rule', () => { - const payload: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), - concurrent_searches: 10, - items_per_search: 10, - }; + test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validation fails without them', () => { + /* eslint-disable @typescript-eslint/naming-convention */ + const { + threat_index, + threat_query, + threat_mapping, + ...payload + } = getCreateThreatMatchRulesSchemaMock(); const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected: CreateRulesSchemaDecoded = { - ...getCreateThreatMatchRulesSchemaDecodedMock(), - concurrent_searches: 10, - items_per_search: 10, - }; - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threat_query"', + 'Invalid value "undefined" supplied to "threat_mapping"', + 'Invalid value "undefined" supplied to "threat_index"', + ]); + expect(message.schema).toEqual({}); + }); + + test('fails validation when threat_mapping is an empty array', () => { + const payload = getCreateThreatMatchRulesSchemaMock(); + payload.threat_mapping = []; + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "threat_mapping"', + ]); + expect(message.schema).toEqual({}); }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts new file mode 100644 index 0000000000000..799b06f169df5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -0,0 +1,442 @@ +/* + * 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 * as t from 'io-ts'; + +import { listArray } from '../types/lists'; +import { + threat_filters, + threat_query, + threat_mapping, + threat_index, + concurrent_searches, + items_per_search, +} from '../types/threat_mapping'; +import { + id, + index, + filters, + event_category_override, + risk_score_mapping, + severity_mapping, + building_block_type, + note, + license, + timeline_id, + timeline_title, + meta, + rule_name_override, + timestamp_override, + author, + description, + false_positives, + from, + rule_id, + immutable, + output_index, + query, + machine_learning_job_id, + max_signals, + risk_score, + severity, + threat, + to, + references, + version, + saved_id, + threshold, + anomaly_threshold, + name, + tags, + actions, + interval, + enabled, + updated_at, + created_at, + job_status, + status_date, + last_success_at, + last_success_message, + last_failure_at, + last_failure_message, + throttleOrNull, + createdByOrNull, + updatedByOrNull, +} from '../common/schemas'; + +const createSchema = < + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +>( + requiredFields: Required, + optionalFields: Optional, + defaultableFields: Defaultable +) => { + return t.intersection([ + t.exact(t.type(requiredFields)), + t.exact(t.partial(optionalFields)), + t.exact(t.partial(defaultableFields)), + ]); +}; + +const patchSchema = < + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +>( + requiredFields: Required, + optionalFields: Optional, + defaultableFields: Defaultable +) => { + return t.intersection([ + t.exact(t.partial(requiredFields)), + t.exact(t.partial(optionalFields)), + t.exact(t.partial(defaultableFields)), + ]); +}; + +const responseSchema = < + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +>( + requiredFields: Required, + optionalFields: Optional, + defaultableFields: Defaultable +) => { + return t.intersection([ + t.exact(t.type(requiredFields)), + t.exact(t.partial(optionalFields)), + t.exact(t.type(defaultableFields)), + ]); +}; + +const buildAPISchemas = ( + params: APIParams +) => { + return { + create: createSchema(params.required, params.optional, params.defaultable), + patch: patchSchema(params.required, params.optional, params.defaultable), + response: responseSchema(params.required, params.optional, params.defaultable), + }; +}; + +interface APIParams< + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +> { + required: Required; + optional: Optional; + defaultable: Defaultable; +} + +const commonParams = { + required: { + name, + description, + risk_score, + severity, + }, + optional: { + building_block_type, + note, + license, + output_index, + timeline_id, + timeline_title, + meta, + rule_name_override, + timestamp_override, + }, + defaultable: { + tags, + interval, + enabled, + throttle: throttleOrNull, + actions, + author, + false_positives, + from, + rule_id, + // maxSignals not used in ML rules but probably should be used + max_signals, + risk_score_mapping, + severity_mapping, + threat, + to, + references, + version, + exceptions_list: listArray, + }, +}; +const { + create: commonCreateParams, + patch: commonPatchParams, + response: commonResponseParams, +} = buildAPISchemas(commonParams); + +const eqlRuleParams = { + required: { + type: t.literal('eql'), + language: t.literal('eql'), + query, + }, + optional: { + index, + filters, + event_category_override, + }, + defaultable: {}, +}; +const { + create: eqlCreateParams, + patch: eqlPatchParams, + response: eqlResponseParams, +} = buildAPISchemas(eqlRuleParams); + +const threatMatchRuleParams = { + required: { + type: t.literal('threat_match'), + query, + threat_query, + threat_mapping, + threat_index, + }, + optional: { + index, + filters, + saved_id, + threat_filters, + threat_language: t.keyof({ kuery: null, lucene: null }), + concurrent_searches, + items_per_search, + }, + defaultable: { + language: t.keyof({ kuery: null, lucene: null }), + }, +}; +const { + create: threatMatchCreateParams, + patch: threatMatchPatchParams, + response: threatMatchResponseParams, +} = buildAPISchemas(threatMatchRuleParams); + +const queryRuleParams = { + required: { + type: t.literal('query'), + }, + optional: { + index, + filters, + saved_id, + }, + defaultable: { + query, + language: t.keyof({ kuery: null, lucene: null }), + }, +}; +const { + create: queryCreateParams, + patch: queryPatchParams, + response: queryResponseParams, +} = buildAPISchemas(queryRuleParams); + +const savedQueryRuleParams = { + required: { + type: t.literal('saved_query'), + saved_id, + }, + optional: { + // Having language, query, and filters possibly defined adds more code confusion and probably user confusion + // if the saved object gets deleted for some reason + index, + query, + filters, + }, + defaultable: { + language: t.keyof({ kuery: null, lucene: null }), + }, +}; +const { + create: savedQueryCreateParams, + patch: savedQueryPatchParams, + response: savedQueryResponseParams, +} = buildAPISchemas(savedQueryRuleParams); + +const thresholdRuleParams = { + required: { + type: t.literal('threshold'), + query, + threshold, + }, + optional: { + index, + filters, + saved_id, + }, + defaultable: { + language: t.keyof({ kuery: null, lucene: null }), + }, +}; +const { + create: thresholdCreateParams, + patch: thresholdPatchParams, + response: thresholdResponseParams, +} = buildAPISchemas(thresholdRuleParams); + +const machineLearningRuleParams = { + required: { + type: t.literal('machine_learning'), + anomaly_threshold, + machine_learning_job_id, + }, + optional: {}, + defaultable: {}, +}; +const { + create: machineLearningCreateParams, + patch: machineLearningPatchParams, + response: machineLearningResponseParams, +} = buildAPISchemas(machineLearningRuleParams); + +const createTypeSpecific = t.union([ + eqlCreateParams, + threatMatchCreateParams, + queryCreateParams, + savedQueryCreateParams, + thresholdCreateParams, + machineLearningCreateParams, +]); +export type CreateTypeSpecific = t.TypeOf; + +// Convenience types for building specific types of rules +export const eqlCreateSchema = t.intersection([eqlCreateParams, commonCreateParams]); +export type EqlCreateSchema = t.TypeOf; + +export const threatMatchCreateSchema = t.intersection([ + threatMatchCreateParams, + commonCreateParams, +]); +export type ThreatMatchCreateSchema = t.TypeOf; + +export const queryCreateSchema = t.intersection([queryCreateParams, commonCreateParams]); +export type QueryCreateSchema = t.TypeOf; + +export const savedQueryCreateSchema = t.intersection([savedQueryCreateParams, commonCreateParams]); +export type SavedQueryCreateSchema = t.TypeOf; + +export const thresholdCreateSchema = t.intersection([thresholdCreateParams, commonCreateParams]); +export type ThresholdCreateSchema = t.TypeOf; + +export const machineLearningCreateSchema = t.intersection([ + machineLearningCreateParams, + commonCreateParams, +]); +export type MachineLearningCreateSchema = t.TypeOf; + +export const createRulesSchema = t.intersection([commonCreateParams, createTypeSpecific]); +export type CreateRulesSchema = t.TypeOf; + +export const eqlUpdateSchema = t.intersection([ + eqlCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type EqlUpdateSchema = t.TypeOf; + +export const threatMatchUpdateSchema = t.intersection([ + threatMatchCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type ThreatMatchUpdateSchema = t.TypeOf; + +export const queryUpdateSchema = t.intersection([ + queryCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type QueryUpdateSchema = t.TypeOf; + +export const savedQueryUpdateSchema = t.intersection([ + savedQueryCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type SavedQueryUpdateSchema = t.TypeOf; + +export const thresholdUpdateSchema = t.intersection([ + thresholdCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type ThresholdUpdateSchema = t.TypeOf; + +export const machineLearningUpdateSchema = t.intersection([ + machineLearningCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type MachineLearningUpdateSchema = t.TypeOf; + +const patchTypeSpecific = t.union([ + eqlPatchParams, + threatMatchPatchParams, + queryPatchParams, + savedQueryPatchParams, + thresholdPatchParams, + machineLearningPatchParams, +]); + +const responseTypeSpecific = t.union([ + eqlResponseParams, + threatMatchResponseParams, + queryResponseParams, + savedQueryResponseParams, + thresholdResponseParams, + machineLearningResponseParams, +]); +export type ResponseTypeSpecific = t.TypeOf; + +export const updateRulesSchema = t.intersection([ + commonCreateParams, + createTypeSpecific, + t.exact(t.partial({ id })), +]); +export type UpdateRulesSchema = t.TypeOf; + +export const fullPatchSchema = t.intersection([ + commonPatchParams, + patchTypeSpecific, + t.exact(t.partial({ id })), +]); + +const responseRequiredFields = { + id, + immutable, + updated_at, + updated_by: updatedByOrNull, + created_at, + created_by: createdByOrNull, +}; +const responseOptionalFields = { + status: job_status, + status_date, + last_success_at, + last_success_message, + last_failure_at, + last_failure_message, +}; + +export const fullResponseSchema = t.intersection([ + commonResponseParams, + responseTypeSpecific, + t.exact(t.type(responseRequiredFields)), + t.exact(t.partial(responseOptionalFields)), +]); +export type FullResponseSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts index a82a3abc8c0ac..e3ad450eaa844 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts @@ -8,11 +8,8 @@ import { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bul import { exactCheck } from '../../../exact_check'; import { foldLeftRight } from '../../../test_utils'; import { formatErrors } from '../../../format_errors'; -import { - getUpdateRulesSchemaMock, - getUpdateRulesSchemaDecodedMock, -} from './update_rules_schema.mock'; -import { UpdateRulesSchema } from './update_rules_schema'; +import { getUpdateRulesSchemaMock } from './rule_schemas.mock'; +import { UpdateRulesSchema } from './rule_schemas'; // only the basics of testing are here. // see: update_rules_schema.test.ts for the bulk of the validation tests @@ -34,13 +31,16 @@ describe('update_rules_bulk_schema', () => { const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "description"' + ); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "risk_score"' + ); + expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"'); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "severity"' + ); expect(output.schema).toEqual({}); }); @@ -51,7 +51,7 @@ describe('update_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([getUpdateRulesSchemaDecodedMock()]); + expect(output.schema).toEqual(payload); }); test('two array elements do validate', () => { @@ -61,10 +61,7 @@ describe('update_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - getUpdateRulesSchemaDecodedMock(), - getUpdateRulesSchemaDecodedMock(), - ]); + expect(output.schema).toEqual(payload); }); test('single array element with a missing value (risk_score) will not validate', () => { @@ -138,7 +135,7 @@ describe('update_rules_bulk_schema', () => { madeUpValue: 'something', }; const secondItem = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + const payload = [singleItem, secondItem]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -180,28 +177,6 @@ describe('update_rules_bulk_schema', () => { expect(output.schema).toEqual({}); }); - test('The default for "from" will be "now-6m"', () => { - const { from, ...withoutFrom } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutFrom]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].from).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...withoutTo } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutTo]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].to).toEqual('now'); - }); - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { const badSeverity = { ...getUpdateRulesSchemaMock(), severity: 'madeup' }; const payload = [badSeverity]; @@ -222,9 +197,7 @@ describe('update_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - { ...getUpdateRulesSchemaDecodedMock(), note: '# test markdown' }, - ]); + expect(output.schema).toEqual(payload); }); test('You can set "note" to an empty string', () => { @@ -234,10 +207,10 @@ describe('update_rules_bulk_schema', () => { const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([{ ...getUpdateRulesSchemaDecodedMock(), note: '' }]); + expect(output.schema).toEqual(payload); }); - test('You can set "note" to anything other than string', () => { + test('You cant set "note" to anything other than string', () => { const payload = [ { ...getUpdateRulesSchemaMock(), @@ -255,26 +228,4 @@ describe('update_rules_bulk_schema', () => { ]); expect(output.schema).toEqual({}); }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...withoutActions } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutActions]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].actions).toEqual([]); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...withoutThrottle } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutThrottle]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].throttle).toEqual(null); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts index 429103c7df13e..f1d7c60991605 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts @@ -5,10 +5,7 @@ */ import * as t from 'io-ts'; - -import { updateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema'; +import { updateRulesSchema } from './rule_schemas'; export const updateRulesBulkSchema = t.array(updateRulesSchema); export type UpdateRulesBulkSchema = t.TypeOf; - -export type UpdateRulesBulkSchemaDecoded = UpdateRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts deleted file mode 100644 index b3fbf96188352..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UpdateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; - -export const getUpdateRulesSchemaMock = (): UpdateRulesSchema => ({ - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', -}); - -export const getUpdateRulesSchemaDecodedMock = (): UpdateRulesSchemaDecoded => ({ - author: [], - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - severity_mapping: [], - type: 'query', - risk_score: 55, - risk_score_mapping: [], - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - from: 'now-6m', - interval: '5m', - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - to: 'now', - threat: [], - throttle: null, - exceptions_list: [], - rule_id: 'rule-1', -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts deleted file mode 100644 index e3347b41ac0fa..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts +++ /dev/null @@ -1,1628 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - updateRulesSchema, - UpdateRulesSchema, - UpdateRulesSchemaDecoded, -} from './update_rules_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { - getUpdateRulesSchemaMock, - getUpdateRulesSchemaDecodedMock, -} from './update_rules_schema.mock'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; -import { getListArrayMock } from '../types/lists.mock'; - -describe('update rules schema', () => { - test('empty objects do not validate', () => { - const payload: Partial = {}; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('made up values do not validate', () => { - const payload: UpdateRulesSchema & { madeUp: string } = { - ...getUpdateRulesSchemaMock(), - madeUp: 'hi', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']); - expect(message.schema).toEqual({}); - }); - - test('[rule_id] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('name cannot be an empty string', () => { - const payload: UpdateRulesSchema = { - description: 'some description', - name: '', - risk_score: 50, - severity: 'low', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "name"']); - expect(message.schema).toEqual({}); - }); - - test('description cannot be an empty string', () => { - const payload: UpdateRulesSchema = { - description: '', - name: 'rule name', - risk_score: 50, - severity: 'low', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "description"']); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('You can send in an empty array to threat', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - threat: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - threat: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('allows references to be sent as valid', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - references: ['index-1'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - references: ['index-1'], - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults references to an array if it is not sent in', () => { - const { references, ...noReferences } = getUpdateRulesSchemaMock(); - const decoded = updateRulesSchema.decode(noReferences); - const checked = exactCheck(noReferences, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - references: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('references cannot be numbers', () => { - const payload: Omit & { references: number[] } = { - ...getUpdateRulesSchemaMock(), - references: [5], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "references"']); - expect(message.schema).toEqual({}); - }); - - test('indexes cannot be numbers', () => { - const payload: Omit & { index: number[] } = { - ...getUpdateRulesSchemaMock(), - index: [5], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "index"']); - expect(message.schema).toEqual({}); - }); - - test('defaults interval to 5 min', () => { - const { interval, ...noInterval } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noInterval, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { interval: expectedInterval, ...expectedNoInterval } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoInterval, - interval: '5m', - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults max signals to 100', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { max_signals, ...noMaxSignals } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noMaxSignals, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { - max_signals: expectedMaxSignals, - ...expectedNoMaxSignals - } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoMaxSignals, - max_signals: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('saved_query type can have filters with it', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - filters: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('filters cannot be a string', () => { - const payload: Omit & { filters: string } = { - ...getUpdateRulesSchemaMock(), - filters: 'some string', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "filters"', - ]); - expect(message.schema).toEqual({}); - }); - - test('language validates with kuery', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - language: 'kuery', - }; - expect(message.schema).toEqual(expected); - }); - - test('language validates with lucene', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - language: 'lucene', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - language: 'lucene', - }; - expect(message.schema).toEqual(expected); - }); - - test('language does not validate with something made up', () => { - const payload: Omit & { language: string } = { - ...getUpdateRulesSchemaMock(), - language: 'something-made-up', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "something-made-up" supplied to "language"', - ]); - expect(message.schema).toEqual({}); - }); - - test('max_signals cannot be negative', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - max_signals: -1, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "max_signals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('max_signals cannot be zero', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - max_signals: 0, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to "max_signals"']); - expect(message.schema).toEqual({}); - }); - - test('max_signals can be 1', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - max_signals: 1, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - max_signals: 1, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can optionally send in an array of tags', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - tags: ['tag_1', 'tag_2'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - tags: ['tag_1', 'tag_2'], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of tags that are numbers', () => { - const payload: Omit & { tags: number[] } = { - ...getUpdateRulesSchemaMock(), - tags: [0, 1, 2], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "tags"', - 'Invalid value "1" supplied to "tags"', - 'Invalid value "2" supplied to "tags"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getUpdateRulesSchemaMock(), - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,framework"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getUpdateRulesSchemaMock(), - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,tactic"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getUpdateRulesSchemaMock(), - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You can optionally send in an array of false positives', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - false_positives: ['false_1', 'false_2'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - false_positives: ['false_1', 'false_2'], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - const payload: Omit & { false_positives: number[] } = { - ...getUpdateRulesSchemaMock(), - false_positives: [5, 4], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "false_positives"', - 'Invalid value "4" supplied to "false_positives"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the immutable to a number when trying to update a rule', () => { - const payload: Omit & { immutable: number } = { - ...getUpdateRulesSchemaMock(), - immutable: 5, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "immutable"']); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the risk_score to 101', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: 101, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "101" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the risk_score to -1', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: -1, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "risk_score"']); - expect(message.schema).toEqual({}); - }); - - test('You can set the risk_score to 0', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: 0, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - risk_score: 0, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set the risk_score to 100', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: 100, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - risk_score: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set meta to any object you want', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot update meta as a string', () => { - const payload: Omit & { meta: string } = { - ...getUpdateRulesSchemaMock(), - meta: 'should not work', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "should not work" supplied to "meta"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You can omit the query string when filters are present', () => { - const { query, ...noQuery } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noQuery, - filters: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { query: expectedQuery, ...expectedNoQuery } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoQuery, - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('validates with timeline_id and timeline_title', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "from" will be "now-6m"', () => { - const { from, ...noFrom } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noFrom, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { from: expectedFrom, ...expectedNoFrom } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoFrom, - from: 'now-6m', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...noTo } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noTo, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { to: expectedTo, ...expectedNoTo } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoTo, - to: 'now', - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const payload: Omit & { severity: string } = { - ...getUpdateRulesSchemaMock(), - severity: 'junk', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to "severity"']); - expect(message.schema).toEqual({}); - }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...noActions } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noActions, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { actions: expectedActions, ...expectedNoActions } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoActions, - actions: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,group"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', params: {} }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,action_type_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,params"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are including "actionTypeId"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,action_type_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...noThrottle } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noThrottle, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { throttle: expectedThrottle, ...expectedNoThrottle } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoThrottle, - throttle: null, - }; - expect(message.schema).toEqual(expected); - }); - - describe('note', () => { - test('You can set note to a string', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - note: '# documentation markdown here', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - note: '# documentation markdown here', - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set note to an empty string', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - note: '', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - note: '', - }; - expect(message.schema).toEqual(expected); - }); - - // Note: If you're looking to remove `note`, omit `note` entirely - test('You cannot set note to null', () => { - const payload: Omit & { note: null } = { - ...getUpdateRulesSchemaMock(), - note: null, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "null" supplied to "note"']); - expect(message.schema).toEqual({}); - }); - - test('You cannot set note as an object', () => { - const payload: Omit & { note: {} } = { - ...getUpdateRulesSchemaMock(), - note: { - somethingHere: 'something else', - }, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "{"somethingHere":"something else"}" supplied to "note"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - }); - - describe('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - exceptions_list: getListArrayMock(), - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - filters: [], - exceptions_list: [ - { - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', - }, - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - exceptions_list: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - filters: [], - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - const payload: Omit & { - exceptions_list: Array<{ id: string; namespace_type: string }>; - } = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "exceptions_list,list_id"', - 'Invalid value "undefined" supplied to "exceptions_list,type"', - 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts deleted file mode 100644 index 5d759fc12cd52..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; - -import { - description, - anomaly_threshold, - filters, - RuleId, - index, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - machine_learning_job_id, - risk_score, - rule_id, - MaxSignals, - name, - severity, - Tags, - To, - type, - Threat, - threshold, - ThrottleOrNull, - note, - version, - References, - Actions, - Enabled, - FalsePositives, - From, - Interval, - language, - query, - id, - building_block_type, - license, - rule_name_override, - timestamp_override, - Author, - RiskScoreMapping, - SeverityMapping, - event_category_override, -} from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, -} from '../types/threat_mapping'; - -import { - DefaultStringArray, - DefaultActionsArray, - DefaultBooleanTrue, - DefaultFromString, - DefaultIntervalString, - DefaultMaxSignalsNumber, - DefaultToString, - DefaultThreatArray, - DefaultThrottleNull, - DefaultListArray, - ListArray, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; - -/** - * This almost identical to the create_rules_schema except for a few details. - * - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented - * This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in - * this will update the rule's version number. - * - id is on here because you can pass in an id to update using it instead of rule_id. - */ -export const updateRulesSchema = t.intersection([ - t.exact( - t.type({ - description, - risk_score, - name, - severity, - type, - }) - ), - t.exact( - t.partial({ - id, // defaults to "undefined" if not set during decode - actions: DefaultActionsArray, // defaults to empty actions array if not set during decode - anomaly_threshold, // defaults to undefined if not set during decode - author: DefaultStringArray, // defaults to empty array of strings if not set during decode - building_block_type, // defaults to undefined if not set during decode - enabled: DefaultBooleanTrue, // defaults to true if not set during decode - event_category_override, - false_positives: DefaultStringArray, // defaults to empty string array if not set during decode - filters, // defaults to undefined if not set during decode - from: DefaultFromString, // defaults to "now-6m" if not set during decode - rule_id, // defaults to "undefined" if not set during decode - index, // defaults to undefined if not set during decode - interval: DefaultIntervalString, // defaults to "5m" if not set during decode - query, // defaults to undefined if not set during decode - language, // defaults to undefined if not set during decode - license, // defaults to "undefined" if not set during decode - // TODO: output_index: This should be removed eventually - output_index, // defaults to "undefined" if not set during decode - saved_id, // defaults to "undefined" if not set during decode - timeline_id, // defaults to "undefined" if not set during decode - timeline_title, // defaults to "undefined" if not set during decode - meta, // defaults to "undefined" if not set during decode - machine_learning_job_id, // defaults to "undefined" if not set during decode - max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode - risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode - rule_name_override, // defaults to "undefined" if not set during decode - severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode - tags: DefaultStringArray, // defaults to empty string array if not set during decode - to: DefaultToString, // defaults to "now" if not set during decode - threat: DefaultThreatArray, // defaults to empty array if not set during decode - threshold, // defaults to "undefined" if not set during decode - throttle: DefaultThrottleNull, // defaults to "null" if not set during decode - timestamp_override, // defaults to "undefined" if not set during decode - references: DefaultStringArray, // defaults to empty array of strings if not set during decode - note, // defaults to "undefined" if not set during decode - version, // defaults to "undefined" if not set during decode - exceptions_list: DefaultListArray, // defaults to empty array if not set during decode - threat_mapping, // defaults to "undefined" if not set during decode - threat_query, // defaults to "undefined" if not set during decode - threat_filters, // defaults to "undefined" if not set during decode - threat_index, // defaults to "undefined" if not set during decode - threat_language, // defaults "undefined" if not set during decode - concurrent_searches, // defaults to "undefined" if not set during decode - items_per_search, // defaults to "undefined" if not set during decode - }) - ), -]); - -export type UpdateRulesSchema = t.TypeOf; - -// This type is used after a decode since some things are defaults after a decode. -export type UpdateRulesSchemaDecoded = Omit< - UpdateRulesSchema, - | 'author' - | 'references' - | 'actions' - | 'enabled' - | 'false_positives' - | 'from' - | 'interval' - | 'max_signals' - | 'risk_score_mapping' - | 'severity_mapping' - | 'tags' - | 'to' - | 'threat' - | 'throttle' - | 'exceptions_list' - | 'rule_id' -> & { - author: Author; - references: References; - actions: Actions; - enabled: Enabled; - false_positives: FalsePositives; - from: From; - interval: Interval; - max_signals: MaxSignals; - risk_score_mapping: RiskScoreMapping; - severity_mapping: SeverityMapping; - tags: Tags; - to: To; - threat: Threat; - throttle: ThrottleOrNull; - exceptions_list: ListArray; - rule_id: RuleId; -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts index 91b11ea758e93..c246a1bff9f64 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts @@ -4,28 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getUpdateRulesSchemaMock } from './update_rules_schema.mock'; -import { UpdateRulesSchema } from './update_rules_schema'; +import { getUpdateRulesSchemaMock } from './rule_schemas.mock'; +import { UpdateRulesSchema } from './rule_schemas'; import { updateRuleValidateTypeDependents } from './update_rules_type_dependents'; describe('update_rules_type_dependents', () => { - test('saved_id is required when type is saved_query and will not validate without out', () => { - const schema: UpdateRulesSchema = { ...getUpdateRulesSchemaMock(), type: 'saved_query' }; - delete schema.saved_id; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "saved_query", "saved_id" is required']); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - const schema: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - type: 'saved_query', - saved_id: '123', - }; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual([]); - }); - test('You cannot omit timeline_title when timeline_id is present', () => { const schema: UpdateRulesSchema = { ...getUpdateRulesSchemaMock(), @@ -85,26 +68,4 @@ describe('update_rules_type_dependents', () => { const errors = updateRuleValidateTypeDependents(schema); expect(errors).toEqual(['either "id" or "rule_id" must be set']); }); - - test('threshold is required when type is threshold and validates with it', () => { - const schema: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - type: 'threshold', - }; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); - }); - - test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { - const schema: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - type: 'threshold', - threshold: { - field: '', - value: -1, - }, - }; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index 5f297fb9688fc..e68ffd7925709 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -4,69 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isMlRule } from '../../../machine_learning/helpers'; -import { isThresholdRule } from '../../utils'; -import { UpdateRulesSchema } from './update_rules_schema'; - -export const validateAnomalyThreshold = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.anomaly_threshold == null) { - return ['when "type" is "machine_learning" anomaly_threshold is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateQuery = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.query != null) { - return ['when "type" is "machine_learning", "query" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateLanguage = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.language != null) { - return ['when "type" is "machine_learning", "language" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateSavedId = (rule: UpdateRulesSchema): string[] => { - if (rule.type === 'saved_query') { - if (rule.saved_id == null) { - return ['when "type" is "saved_query", "saved_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateMachineLearningJobId = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.machine_learning_job_id == null) { - return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; +import { UpdateRulesSchema } from './rule_schemas'; export const validateTimelineId = (rule: UpdateRulesSchema): string[] => { if (rule.timeline_id != null) { @@ -104,29 +42,6 @@ export const validateId = (rule: UpdateRulesSchema): string[] => { } }; -export const validateThreshold = (rule: UpdateRulesSchema): string[] => { - if (isThresholdRule(rule.type)) { - if (!rule.threshold) { - return ['when "type" is "threshold", "threshold" is required']; - } else if (rule.threshold.value <= 0) { - return ['"threshold.value" has to be bigger than 0']; - } else { - return []; - } - } - return []; -}; - export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => { - return [ - ...validateId(schema), - ...validateAnomalyThreshold(schema), - ...validateQuery(schema), - ...validateLanguage(schema), - ...validateSavedId(schema), - ...validateMachineLearningJobId(schema), - ...validateTimelineId(schema), - ...validateTimelineTitle(schema), - ...validateThreshold(schema), - ]; + return [...validateId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema)]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts index e76dd3fca3740..de0625e6b5817 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts @@ -28,6 +28,7 @@ export * from './default_version_number'; export * from './iso_date_string'; export * from './lists'; export * from './lists_default_array'; +export * from './non_empty_array'; export * from './non_empty_string'; export * from './only_false_allowed'; export * from './positive_integer'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts new file mode 100644 index 0000000000000..299cc92407983 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts @@ -0,0 +1,94 @@ +/* + * 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 * as t from 'io-ts'; + +import { NonEmptyArray } from './non_empty_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +const testSchema = t.keyof({ + valid: true, + also_valid: true, +}); +type TestSchema = t.TypeOf; + +const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray'); + +describe('non empty array', () => { + test('it should generate the correct name for non empty array', () => { + const newTestSchema = NonEmptyArray(testSchema); + expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">'); + }); + + test('it should use a supplied name override', () => { + const newTestSchema = NonEmptyArray(testSchema, 'someName'); + expect(newTestSchema.name).toEqual('someName'); + }); + + test('it should NOT validate an empty array', () => { + const payload: string[] = []; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of testSchema', () => { + const payload: TestSchema[] = ['valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of valid testSchema strings', () => { + const payload: TestSchema[] = ['valid', 'also_valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['valid', 123]; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "123" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an array with an invalid string', () => { + const payload = ['valid', 'invalid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a null value', () => { + const payload = null; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts new file mode 100644 index 0000000000000..433ee5a4bf080 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts @@ -0,0 +1,28 @@ +/* + * 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 * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export const NonEmptyArray = ( + codec: C, + name: string = `NonEmptyArray<${codec.name}>` +) => { + const arrType = t.array(codec); + type ArrType = t.TypeOf; + return new t.Type( + name, + arrType.is, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + return arrType.validate(input, context); + } + }, + t.identity + ); +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts index d8f61e4309b17..d3e8b95e69c3a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts @@ -176,6 +176,19 @@ describe('threat_mapping', () => { expect(message.schema).toEqual({}); }); + test('it should fail validate with empty array', () => { + const payload: string[] = []; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyArray"', + ]); + expect(message.schema).toEqual({}); + }); + test('it should fail validation when concurrent_searches is < 0', () => { const payload = -1; const decoded = concurrent_searches.decode(payload); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts index dec8ddd000132..ad54d05863ad3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import { language } from '../common/schemas'; +import { NonEmptyArray } from './non_empty_array'; import { NonEmptyString } from './non_empty_string'; import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; @@ -41,7 +42,7 @@ export const threatMap = t.exact( ); export type ThreatMap = t.TypeOf; -export const threat_mapping = t.array(threatMap); +export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); export type ThreatMapping = t.TypeOf; export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 0b708133d947b..e94cc8845c5a5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -22,8 +22,10 @@ import { getPrePackagedRulesStatus, } from './api'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { + getCreateRulesSchemaMock, + getUpdateRulesSchemaMock, +} from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; import { buildEsQuery } from 'src/plugins/data/common'; @@ -64,7 +66,7 @@ describe('Detections Rules API', () => { await updateRule({ rule: payload, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { body: - '{"description":"some description","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}', + '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd"}', method: 'PUT', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index ce1fdd18dbdef..9512ae6f2d6e0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, @@ -42,8 +43,8 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp * * @throws An error if response is not OK */ -export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'POST', body: JSON.stringify(rule), signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx index 42d6a2a92a4c2..0d0047dd06e3f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useCreateRule, ReturnCreateRule } from './use_create_rule'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('./api'); @@ -24,7 +24,7 @@ describe('useCreateRule', () => { useCreateRule() ); await waitForNextUpdate(); - result.current[1](getUpdateRulesSchemaMock()); + result.current[1](getCreateRulesSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -36,7 +36,7 @@ describe('useCreateRule', () => { useCreateRule() ); await waitForNextUpdate(); - result.current[1](getUpdateRulesSchemaMock()); + result.current[1](getCreateRulesSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx index 9603a4151933a..95c5bef962a00 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateRule, ReturnUpdateRule } from './use_update_rule'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('./api'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 11222a0a95a80..239d885bfc157 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,7 @@ */ import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/create_rules_schema'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { Rule } from '../../../../containers/detection_engine/rules'; import { getListMock, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 773e84d9c88fc..fd29be0e81f3c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -26,7 +26,7 @@ import { requestMock } from './request'; import { RuleNotificationAlertType } from '../../notifications/types'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 06fcba36642ca..55317fc28afca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -18,7 +18,7 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -180,9 +180,7 @@ describe('create_rules_bulk', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unexpected_type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('disallows invalid "from" param on rule', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index aa409580df965..b185b8780abe2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -6,18 +6,13 @@ import { validate } from '../../../../../common/validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { - CreateRulesBulkSchemaDecoded, - createRulesBulkSchema, -} from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; +import { createRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; -import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; import { getDuplicates } from './utils'; import { transformValidateBulkError } from './validate'; @@ -26,17 +21,14 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { PartialFilter } from '../../types'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters'; export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, validate: { - body: buildRouteValidation( - createRulesBulkSchema - ), + body: buildRouteValidation(createRulesBulkSchema), }, options: { tags: ['access:securitySolution'], @@ -67,158 +59,63 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => ruleDefinitions .filter((rule) => rule.rule_id == null || !dupes.includes(rule.rule_id)) .map(async (payloadRule) => { - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters: filtersRest, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - threat, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_mapping: threatMapping, - threat_query: threatQuery, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - threshold, - throttle, - timestamp_override: timestampOverride, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - exceptions_list: exceptionsList, - } = payloadRule; + if (payloadRule.rule_id != null) { + const rule = await readRules({ + alertsClient, + ruleId: payloadRule.rule_id, + id: undefined, + }); + if (rule != null) { + return createBulkErrorObject({ + ruleId: payloadRule.rule_id, + statusCode: 409, + message: `rule_id: "${payloadRule.rule_id}" already exists`, + }); + } + } + const internalRule = convertCreateAPIToInternalSchema(payloadRule, siemClient); try { const validationErrors = createRuleValidateTypeDependents(payloadRule); if (validationErrors.length) { return createBulkErrorObject({ - ruleId, + ruleId: internalRule.params.ruleId, statusCode: 400, message: validationErrors.join(), }); } - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined; - - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - throwHttpError(await mlAuthz.validateRuleType(type)); - - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); + throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type)); + const finalIndex = internalRule.params.outputIndex; const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ - ruleId, + ruleId: internalRule.params.ruleId, statusCode: 400, message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, }); } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId, id: undefined }); - if (rule != null) { - return createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }); - } - } - const createdRule = await createRules({ - alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - immutable: false, - query, - language, - license, - machineLearningJobId, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - name, - riskScore, - riskScoreMapping, - ruleNameOverride, - severity, - severityMapping, - tags, - to, - type, - threat, - threatFilters, - threatMapping, - threatQuery, - threatIndex, - threatLanguage, - concurrentSearches, - itemsPerSearch, - threshold, - timestampOverride, - references, - note, - version, - exceptionsList, - actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it, + + const createdRule = await alertsClient.create({ + data: internalRule, }); const ruleActions = await updateRulesNotifications({ ruleAlertId: createdRule.id, alertsClient, savedObjectsClient, - enabled, - actions, - throttle, - name, + enabled: createdRule.enabled, + actions: payloadRule.actions, + throttle: payloadRule.throttle ?? null, + name: createdRule.name, }); - return transformValidateBulkError(ruleId, createdRule, ruleActions); + return transformValidateBulkError( + internalRule.params.ruleId, + createdRule, + ruleActions + ); } catch (err) { - return transformBulkError(ruleId, err); + return transformBulkError(internalRule.params.ruleId, err); } }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 26febb0999ac7..40465f4dc7456 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -20,7 +20,7 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../rules/update_rules_notifications'); jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -160,9 +160,7 @@ describe('create_rules', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unexpected_type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('allows rule type of query and custom from and interval', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 97c05b4626ddc..b52248f670188 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -4,37 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqlRule } from '../../../../../common/detection_engine/utils'; -import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { - createRulesSchema, - CreateRulesSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/create_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; -import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; -import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { transformError, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -import { PartialFilter } from '../../types'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; +import { newTransformValidate } from './validate'; +import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; +import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters'; export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => { router.post( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation( - createRulesSchema - ), + body: buildRouteValidation(createRulesSchema), }, options: { tags: ['access:securitySolution'], @@ -46,66 +37,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } - - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - machine_learning_job_id: machineLearningJobId, - filters: filtersRest, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - throttle, - timestamp_override: timestampOverride, - to, - type, - references, - note, - exceptions_list: exceptionsList, - } = request.body; try { - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && !isEqlRule(type) && languageOrUndefined == null - ? 'kuery' - : languageOrUndefined; - - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; const alertsClient = context.alerting?.getAlertsClient(); const clusterClient = context.core.elasticsearch.legacy.client; const savedObjectsClient = context.core.savedObjects.client; @@ -115,93 +47,56 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void return siemResponse.error({ statusCode: 404 }); } + if (request.body.rule_id != null) { + const rule = await readRules({ + alertsClient, + ruleId: request.body.rule_id, + id: undefined, + }); + if (rule != null) { + return siemResponse.error({ + statusCode: 409, + body: `rule_id: "${request.body.rule_id}" already exists`, + }); + } + } + + const internalRule = convertCreateAPIToInternalSchema(request.body, siemClient); + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request, savedObjectsClient, }); - throwHttpError(await mlAuthz.validateRuleType(type)); + throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type)); - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + internalRule.params.outputIndex + ); if (!indexExists) { return siemResponse.error({ statusCode: 400, - body: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + body: `To create a rule, the index must exist first. Index ${internalRule.params.outputIndex} does not exist`, }); } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId, id: undefined }); - if (rule != null) { - return siemResponse.error({ - statusCode: 409, - body: `rule_id: "${ruleId}" already exists`, - }); - } - } + // This will create the endpoint list if it does not exist yet await context.lists?.getExceptionListClient().createEndpointList(); - const createdRule = await createRules({ - alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - immutable: false, - query, - language, - license, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - machineLearningJobId, - filters, - ruleId, - index, - interval, - maxSignals, - name, - riskScore, - riskScoreMapping, - ruleNameOverride, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version: 1, - exceptionsList, - actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it, + const createdRule = await alertsClient.create({ + data: internalRule, }); const ruleActions = await updateRulesNotifications({ ruleAlertId: createdRule.id, alertsClient, savedObjectsClient, - enabled, - actions, - throttle, - name, + enabled: createdRule.enabled, + actions: request.body.actions, + throttle: request.body.throttle ?? null, + name: createdRule.name, }); const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ @@ -211,7 +106,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void search: `${createdRule.id}`, searchFields: ['alertId'], }); - const [validated, errors] = transformValidate( + const [validated, errors] = newTransformValidate( createdRule, ruleActions, ruleStatuses.saved_objects[0] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index c162caa1278e6..e1ef22c453185 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -16,7 +16,7 @@ import { } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -60,6 +60,7 @@ describe('patch_rules_bulk', () => { path: `${DETECTION_ENGINE_RULES_URL}/bulk_update`, body: [ { + type: 'machine_learning', rule_id: 'my-rule-id', anomaly_threshold: 4, machine_learning_job_id: 'some_job_id', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index a406de593652b..4d1d510e46a09 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -90,6 +90,7 @@ describe('patch_rules', () => { method: 'patch', path: DETECTION_ENGINE_RULES_URL, body: { + type: 'machine_learning', rule_id: 'my-rule-id', anomaly_threshold: 4, machine_learning_job_id: 'some_job_id', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index ec5a2be255a2c..72583a5a78709 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -18,7 +18,7 @@ import { import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -150,9 +150,7 @@ describe('update_rules_bulk', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unknown_type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('allows rule type of query and custom from and interval', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index f4a31c2bb456d..5f9789220bc10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -6,14 +6,9 @@ import { validate } from '../../../../../common/validate'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { - updateRulesBulkSchema, - UpdateRulesBulkSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema'; +import { updateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -25,16 +20,13 @@ import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '.. import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -import { PartialFilter } from '../../types'; export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.put( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, validate: { - body: buildRouteValidation( - updateRulesBulkSchema - ), + body: buildRouteValidation(updateRulesBulkSchema), }, options: { tags: ['access:securitySolution'], @@ -61,139 +53,34 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async (payloadRule) => { - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersRest, - rule_id: ruleId, - id, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - to, - type, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - throttle, - timestamp_override: timestampOverride, - references, - note, - version, - exceptions_list: exceptionsList, - } = payloadRule; - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); - const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)'; try { const validationErrors = updateRuleValidateTypeDependents(payloadRule); if (validationErrors.length) { return createBulkErrorObject({ - ruleId, + ruleId: payloadRule.rule_id, statusCode: 400, message: validationErrors.join(), }); } - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined; - - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - - throwHttpError(await mlAuthz.validateRuleType(type)); + throwHttpError(await mlAuthz.validateRuleType(payloadRule.type)); const rule = await updateRules({ alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - query, - language, - license, - machineLearningJobId, - outputIndex: finalIndex, - savedId, savedObjectsClient, - timelineId, - timelineTitle, - meta, - filters, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList, - actions, + defaultOutputIndex: siemClient.getSignalsIndex(), + ruleUpdate: payloadRule, }); if (rule != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled, - actions, - throttle, - name, + enabled: payloadRule.enabled ?? true, + actions: payloadRule.actions, + throttle: payloadRule.throttle, + name: payloadRule.name, }); const ruleStatuses = await ruleStatusClient.find({ perPage: 1, @@ -204,7 +91,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => }); return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { - return getIdBulkError({ id, ruleId }); + return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id }); } } catch (err) { return transformBulkError(idOrRuleIdOrUnknown, err); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index fd077c18b7983..96710b6f1d763 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -19,7 +19,7 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesRoute } from './update_rules_route'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/update_rules_notifications'); @@ -131,7 +131,7 @@ describe('update_rules', () => { path: DETECTION_ENGINE_RULES_URL, body: { ...getUpdateRulesSchemaMock(), - rule_id: undefined, + id: undefined, }, }); const response = await server.inject(noIdRequest, context); @@ -160,9 +160,7 @@ describe('update_rules', () => { }); const result = await server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unknown type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('allows rule type of query and custom from and interval', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 7ad525b67f7aa..aa85747e3ce41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { - updateRulesSchema, - UpdateRulesSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/update_rules_schema'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -23,16 +18,13 @@ import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { PartialFilter } from '../../types'; export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.put( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation( - updateRulesSchema - ), + body: buildRouteValidation(updateRulesSchema), }, options: { tags: ['access:securitySolution'], @@ -44,67 +36,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } - - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersRest, - rule_id: ruleId, - id, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - to, - type, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - throttle, - timestamp_override: timestampOverride, - references, - note, - version, - exceptions_list: exceptionsList, - } = request.body; try { - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined; - - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); @@ -120,59 +52,13 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { request, savedObjectsClient, }); - throwHttpError(await mlAuthz.validateRuleType(type)); + throwHttpError(await mlAuthz.validateRuleType(request.body.type)); - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const rule = await updateRules({ alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - query, - language, - license, - machineLearningJobId, - outputIndex: finalIndex, - savedId, savedObjectsClient, - timelineId, - timelineTitle, - meta, - filters, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList, - actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it + defaultOutputIndex: siemClient.getSignalsIndex(), + ruleUpdate: request.body, }); if (rule != null) { @@ -180,10 +66,10 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled, - actions, - throttle, - name, + enabled: request.body.enabled ?? true, + actions: request.body.actions, + throttle: request.body.throttle, + name: request.body.name, }); const ruleStatuses = await ruleStatusClient.find({ perPage: 1, @@ -203,7 +89,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { return response.ok({ body: validated ?? {} }); } } else { - const error = getIdError({ id, ruleId }); + const error = getIdError({ id: request.body.id, ruleId: request.body.rule_id }); return siemResponse.error({ body: error.message, statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 13eb7495a898a..0bd6d43cab464 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -27,10 +27,10 @@ import { PartialAlert } from '../../../../../../alerts/server'; import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; -import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -548,7 +548,7 @@ describe('utils', () => { { rule_id: 'value3' }, {}, {}, - ] as CreateRulesBulkSchemaDecoded, + ] as CreateRulesBulkSchema, 'rule_id' ); const expected = ['value2', 'value3']; @@ -562,7 +562,7 @@ describe('utils', () => { { rule_id: 'value3' }, {}, {}, - ] as CreateRulesBulkSchemaDecoded, + ] as CreateRulesBulkSchema, 'rule_id' ); const expected: string[] = []; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 7360dc77aac22..7a6cd707eb185 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; -import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; +import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { @@ -256,10 +256,7 @@ export const transformOrImportError = ( } }; -export const getDuplicates = ( - ruleDefinitions: CreateRulesBulkSchemaDecoded, - by: 'rule_id' -): string[] => { +export const getDuplicates = (ruleDefinitions: CreateRulesBulkSchema, by: 'rule_id'): string[] => { const mappedDuplicates = countBy( by, ruleDefinitions.filter((r) => r[by] != null) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 27100eaebea15..382186df16cd1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -9,6 +9,10 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { + FullResponseSchema, + fullResponseSchema, +} from '../../../../../common/detection_engine/schemas/request'; import { validate } from '../../../../../common/validate'; import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema'; import { @@ -71,6 +75,19 @@ export const transformValidate = ( } }; +export const newTransformValidate = ( + alert: PartialAlert, + ruleActions?: RuleActions | null, + ruleStatus?: SavedObject +): [FullResponseSchema | null, string | null] => { + const transformed = transform(alert, ruleActions, ruleStatus); + if (transformed == null) { + return [null, 'Internal error transforming']; + } else { + return validate(transformed, fullResponseSchema); + } +}; + export const transformValidateBulkError = ( ruleId: string, alert: PartialAlert, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 22b2593283696..8e10fc21f040c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -5,12 +5,22 @@ */ import { defaults } from 'lodash/fp'; +import { validate } from '../../../../common/validate'; import { PartialAlert } from '../../../../../alerts/server'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PatchRulesOptions } from './types'; import { addTags } from './add_tags'; -import { calculateVersion, calculateName, calculateInterval } from './utils'; +import { calculateVersion, calculateName, calculateInterval, removeUndefined } from './utils'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; +import { internalRuleUpdate } from '../schemas/rule_schemas'; + +class PatchError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} export const patchRules = async ({ alertsClient, @@ -159,18 +169,24 @@ export const patchRules = async ({ } ); + const newRule = { + tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), + throttle: null, + name: calculateName({ updatedName: name, originalName: rule.name }), + schedule: { + interval: calculateInterval(interval, rule.schedule.interval), + }, + actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, + params: removeUndefined(nextParams), + }; + const [validated, errors] = validate(newRule, internalRuleUpdate); + if (errors != null || validated === null) { + throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400); + } + const update = await alertsClient.update({ id: rule.id, - data: { - tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), - throttle: null, - name: calculateName({ updatedName: name, originalName: rule.name }), - schedule: { - interval: calculateInterval(interval, rule.schedule.interval), - }, - actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, - params: nextParams, - }, + data: validated, }); if (rule.enabled && enabled === false) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index f6ab3fb0c3ed2..45186f9978650 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -13,6 +13,7 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; +import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { FalsePositives, @@ -250,55 +251,10 @@ export interface CreateRulesOptions { } export interface UpdateRulesOptions { - id: IdOrUndefined; savedObjectsClient: SavedObjectsClientContract; alertsClient: AlertsClient; - anomalyThreshold: AnomalyThresholdOrUndefined; - author: Author; - buildingBlockType: BuildingBlockTypeOrUndefined; - description: Description; - enabled: Enabled; - eventCategoryOverride: EventCategoryOverrideOrUndefined; - falsePositives: FalsePositives; - from: From; - query: QueryOrUndefined; - language: LanguageOrUndefined; - savedId: SavedIdOrUndefined; - timelineId: TimelineIdOrUndefined; - timelineTitle: TimelineTitleOrUndefined; - meta: MetaOrUndefined; - machineLearningJobId: MachineLearningJobIdOrUndefined; - filters: PartialFilter[]; - ruleId: RuleIdOrUndefined; - index: IndexOrUndefined; - interval: Interval; - license: LicenseOrUndefined; - maxSignals: MaxSignals; - riskScore: RiskScore; - riskScoreMapping: RiskScoreMapping; - ruleNameOverride: RuleNameOverrideOrUndefined; - outputIndex: OutputIndex; - name: Name; - severity: Severity; - severityMapping: SeverityMapping; - tags: Tags; - threat: Threat; - threshold: ThresholdOrUndefined; - threatFilters: ThreatFiltersOrUndefined; - threatIndex: ThreatIndexOrUndefined; - threatQuery: ThreatQueryOrUndefined; - threatMapping: ThreatMappingOrUndefined; - itemsPerSearch: ItemsPerSearchOrUndefined; - concurrentSearches: ConcurrentSearchesOrUndefined; - threatLanguage: ThreatLanguageOrUndefined; - timestampOverride: TimestampOverrideOrUndefined; - to: To; - type: Type; - references: References; - note: NoteOrUndefined; - version: VersionOrUndefined; - exceptionsList: ListArray; - actions: RuleAlertAction[]; + defaultOutputIndex: string; + ruleUpdate: UpdateRulesSchema; } export interface PatchRulesOptions { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index 34be0f6ad843d..ab71110072bfd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -7,107 +7,21 @@ import { UpdateRulesOptions } from './types'; import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { + getUpdateRulesSchemaMock, + getUpdateMachineLearningSchemaMock, +} from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ - author: ['Elastic'], - buildingBlockType: undefined, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), - anomalyThreshold: undefined, - description: 'some description', - enabled: true, - eventCategoryOverride: undefined, - falsePositives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - query: 'user.name: root or user.name: admin', - language: 'kuery', - license: 'Elastic License', - savedId: 'savedId-123', - timelineId: 'timelineid-123', - timelineTitle: 'timeline-title-123', - meta: {}, - machineLearningJobId: undefined, - filters: [], - ruleId: undefined, - index: ['index-123'], - interval: '5m', - maxSignals: 100, - riskScore: 80, - riskScoreMapping: [], - ruleNameOverride: undefined, - outputIndex: 'output-1', - name: 'Query with a rule id', - severity: 'high', - severityMapping: [], - tags: [], - threat: [], - threshold: undefined, - threatFilters: undefined, - threatIndex: undefined, - threatQuery: undefined, - threatMapping: undefined, - threatLanguage: undefined, - timestampOverride: undefined, - concurrentSearches: undefined, - itemsPerSearch: undefined, - to: 'now', - type: 'query', - references: ['http://www.example.com'], - note: '# sample markdown', - version: 1, - exceptionsList: [], - actions: [], + defaultOutputIndex: '.siem-signals-default', + ruleUpdate: getUpdateRulesSchemaMock(), }); export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ - author: ['Elastic'], - buildingBlockType: undefined, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), - anomalyThreshold: 55, - description: 'some description', - enabled: true, - eventCategoryOverride: undefined, - falsePositives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - query: undefined, - language: undefined, - license: 'Elastic License', - savedId: 'savedId-123', - timelineId: 'timelineid-123', - timelineTitle: 'timeline-title-123', - meta: {}, - machineLearningJobId: 'new_job_id', - filters: [], - ruleId: undefined, - index: ['index-123'], - interval: '5m', - maxSignals: 100, - riskScore: 80, - riskScoreMapping: [], - ruleNameOverride: undefined, - outputIndex: 'output-1', - name: 'Machine Learning Job', - severity: 'high', - severityMapping: [], - tags: [], - threat: [], - threshold: undefined, - threatFilters: undefined, - threatIndex: undefined, - threatQuery: undefined, - threatMapping: undefined, - threatLanguage: undefined, - timestampOverride: undefined, - concurrentSearches: undefined, - itemsPerSearch: undefined, - to: 'now', - type: 'machine_learning', - references: ['http://www.example.com'], - note: '# sample markdown', - version: 1, - exceptionsList: [], - actions: [], + defaultOutputIndex: '.siem-signals-default', + ruleUpdate: getUpdateMachineLearningSchemaMock(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts index cf59d2bb5e8c4..31212f4fa9d6b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts @@ -12,61 +12,54 @@ import { AlertsClientMock } from '../../../../../alerts/server/alerts_client.moc describe('updateRules', () => { it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(); - const ruleOptions = { - ...rulesOptionsMock, - enabled: false, - }; - ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(getResult()); + rulesOptionsMock.ruleUpdate.enabled = false; + ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + getResult() + ); - await updateRules(ruleOptions); + await updateRules(rulesOptionsMock); - expect(ruleOptions.alertsClient.disable).toHaveBeenCalledWith( + expect(rulesOptionsMock.alertsClient.disable).toHaveBeenCalledWith( expect.objectContaining({ - id: rulesOptionsMock.id, + id: rulesOptionsMock.ruleUpdate.id, }) ); }); it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(); - const ruleOptions = { - ...rulesOptionsMock, - enabled: true, - }; + rulesOptionsMock.ruleUpdate.enabled = true; - ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({ + ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({ ...getResult(), enabled: false, }); - await updateRules(ruleOptions); + await updateRules(rulesOptionsMock); - expect(ruleOptions.alertsClient.enable).toHaveBeenCalledWith( + expect(rulesOptionsMock.alertsClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - id: rulesOptionsMock.id, + id: rulesOptionsMock.ruleUpdate.id, }) ); }); - it('calls the alertsClient with ML params', async () => { + it('calls the alertsClient with params', async () => { const rulesOptionsMock = getUpdateMlRulesOptionsMock(); - const ruleOptions = { - ...rulesOptionsMock, - enabled: true, - }; + rulesOptionsMock.ruleUpdate.enabled = true; - ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( getMlResult() ); - await updateRules(ruleOptions); + await updateRules(rulesOptionsMock); - expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( + expect(rulesOptionsMock.alertsClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ - anomalyThreshold: rulesOptionsMock.anomalyThreshold, - machineLearningJobId: rulesOptionsMock.machineLearningJobId, + type: 'machine_learning', + severity: 'high', }), }), }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 5168affca5c62..dab8769bcaa65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -4,179 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable complexity */ + +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerts/server'; import { readRules } from './read_rules'; import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; -import { calculateVersion } from './utils'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; +import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; +import { InternalRuleUpdate } from '../schemas/rule_schemas'; export const updateRules = async ({ alertsClient, - author, - buildingBlockType, savedObjectsClient, - description, - eventCategoryOverride, - falsePositives, - enabled, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - from, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - to, - type, - references, - version, - note, - exceptionsList, - anomalyThreshold, - machineLearningJobId, - actions, + defaultOutputIndex, + ruleUpdate, }: UpdateRulesOptions): Promise => { - const rule = await readRules({ alertsClient, ruleId, id }); - if (rule == null) { + const existingRule = await readRules({ + alertsClient, + ruleId: ruleUpdate.rule_id, + id: ruleUpdate.id, + }); + if (existingRule == null) { return null; } - const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { - author, - buildingBlockType, - description, - eventCategoryOverride, - falsePositives, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - from, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - to, - type, - references, - version, - note, - anomalyThreshold, - machineLearningJobId, - exceptionsList, - }); + const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); + const throttle = ruleUpdate.throttle ?? null; + const enabled = ruleUpdate.enabled ?? true; + const newInternalRule: InternalRuleUpdate = { + name: ruleUpdate.name, + tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, false), + params: { + author: ruleUpdate.author ?? [], + buildingBlockType: ruleUpdate.building_block_type, + description: ruleUpdate.description, + ruleId: existingRule.params.ruleId, + falsePositives: ruleUpdate.false_positives ?? [], + from: ruleUpdate.from ?? 'now-6m', + // Unlike the create route, immutable comes from the existing rule here + immutable: existingRule.params.immutable, + license: ruleUpdate.license, + outputIndex: ruleUpdate.output_index ?? defaultOutputIndex, + timelineId: ruleUpdate.timeline_id, + timelineTitle: ruleUpdate.timeline_title, + meta: ruleUpdate.meta, + maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS, + riskScore: ruleUpdate.risk_score, + riskScoreMapping: ruleUpdate.risk_score_mapping ?? [], + ruleNameOverride: ruleUpdate.rule_name_override, + severity: ruleUpdate.severity, + severityMapping: ruleUpdate.severity_mapping ?? [], + threat: ruleUpdate.threat ?? [], + timestampOverride: ruleUpdate.timestamp_override, + to: ruleUpdate.to ?? 'now', + references: ruleUpdate.references ?? [], + note: ruleUpdate.note, + // Always use the version from the request if specified. If it isn't specified, leave immutable rules alone and + // increment the version of mutable rules by 1. + version: + ruleUpdate.version ?? existingRule.params.immutable + ? existingRule.params.version + : existingRule.params.version + 1, + exceptionsList: ruleUpdate.exceptions_list ?? [], + ...typeSpecificParams, + }, + schedule: { interval: ruleUpdate.interval ?? '5m' }, + actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [], + throttle: null, + }; const update = await alertsClient.update({ - id: rule.id, - data: { - tags: addTags(tags, rule.params.ruleId, rule.params.immutable), - name, - schedule: { interval }, - actions: actions.map(transformRuleToAlertAction), - throttle: null, - params: { - author, - buildingBlockType, - description, - ruleId: rule.params.ruleId, - falsePositives, - from, - immutable: rule.params.immutable, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - index, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - severity, - severityMapping, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - timestampOverride, - to, - type, - references, - note, - version: calculatedVersion, - anomalyThreshold, - machineLearningJobId, - exceptionsList, - }, - }, + id: existingRule.id, + data: newInternalRule, }); - if (rule.enabled && enabled === false) { - await alertsClient.disable({ id: rule.id }); - } else if (!rule.enabled && enabled === true) { - await alertsClient.enable({ id: rule.id }); + if (existingRule.enabled && enabled === false) { + await alertsClient.disable({ id: existingRule.id }); + } else if (!existingRule.enabled && enabled === true) { + await alertsClient.enable({ id: existingRule.id }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleCurrentStatus = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', - search: rule.id, + search: existingRule.id, searchFields: ['alertId'], }); @@ -189,6 +104,5 @@ export const updateRules = async ({ }); } } - return { ...update, enabled }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 83d9e3fd3e59f..613e8e474079c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -136,10 +136,7 @@ export const calculateVersion = ( // the version number if only the enabled/disabled flag is being set. Likewise if we get other // properties we are not expecting such as updatedAt we do not to cause a version number bump // on that either. - const removedNullValues = pickBy( - (value: unknown) => value != null, - updateProperties - ); + const removedNullValues = removeUndefined(updateProperties); if (isEmpty(removedNullValues)) { return currentVersion; } else { @@ -147,6 +144,10 @@ export const calculateVersion = ( } }; +export const removeUndefined = (obj: object) => { + return pickBy((value: unknown) => value != null, obj); +}; + export const calculateName = ({ updatedName, originalName, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts new file mode 100644 index 0000000000000..86d85cd2a066e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -0,0 +1,266 @@ +/* + * 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 uuid from 'uuid'; +import { InternalRuleCreate, InternalRuleResponse, TypeSpecificRuleParams } from './rule_schemas'; +import { assertUnreachable } from '../../../../common/utility_types'; +import { + CreateRulesSchema, + CreateTypeSpecific, + FullResponseSchema, + ResponseTypeSpecific, +} from '../../../../common/detection_engine/schemas/request'; +import { RuleActions } from '../rule_actions/types'; +import { AppClient } from '../../../types'; +import { addTags } from '../rules/add_tags'; +import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; + +// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema +// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for +// required and defaultable fields. However, it is still possible to add an optional field to the API schema +// without causing a type-check error here. + +// Converts params from the snake case API format to the internal camel case format AND applies default values where needed. +// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match +// the legacy API behavior +export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecificRuleParams => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + eventCategoryOverride: params.event_category_override, + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threatFilters: params.threat_filters, + threatQuery: params.threat_query, + threatMapping: params.threat_mapping, + threatLanguage: params.threat_language, + threatIndex: params.threat_index, + concurrentSearches: params.concurrent_searches, + itemsPerSearch: params.items_per_search, + }; + } + case 'query': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + query: params.query ?? '', + filters: params.filters, + savedId: params.saved_id, + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language ?? 'kuery', + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threshold: params.threshold, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomalyThreshold: params.anomaly_threshold, + machineLearningJobId: params.machine_learning_job_id, + }; + } + default: { + return assertUnreachable(params); + } + } +}; + +export const convertCreateAPIToInternalSchema = ( + input: CreateRulesSchema, + siemClient: AppClient +): InternalRuleCreate => { + const typeSpecificParams = typeSpecificSnakeToCamel(input); + const newRuleId = input.rule_id ?? uuid.v4(); + return { + name: input.name, + tags: addTags(input.tags ?? [], newRuleId, false), + alertTypeId: SIGNALS_ID, + consumer: SERVER_APP_ID, + params: { + author: input.author ?? [], + buildingBlockType: input.building_block_type, + description: input.description, + ruleId: newRuleId, + falsePositives: input.false_positives ?? [], + from: input.from ?? 'now-6m', + immutable: false, + license: input.license, + outputIndex: input.output_index ?? siemClient.getSignalsIndex(), + timelineId: input.timeline_id, + timelineTitle: input.timeline_title, + meta: input.meta, + maxSignals: input.max_signals ?? DEFAULT_MAX_SIGNALS, + riskScore: input.risk_score, + riskScoreMapping: input.risk_score_mapping ?? [], + ruleNameOverride: input.rule_name_override, + severity: input.severity, + severityMapping: input.severity_mapping ?? [], + threat: input.threat ?? [], + timestampOverride: input.timestamp_override, + to: input.to ?? 'now', + references: input.references ?? [], + note: input.note, + version: input.version ?? 1, + exceptionsList: input.exceptions_list ?? [], + ...typeSpecificParams, + }, + schedule: { interval: input.interval ?? '5m' }, + enabled: input.enabled ?? true, + actions: input.throttle === 'rule' ? (input.actions ?? []).map(transformRuleToAlertAction) : [], + throttle: null, + }; +}; + +// Converts the internal rule data structure to the response API schema +export const typeSpecificCamelToSnake = (params: TypeSpecificRuleParams): ResponseTypeSpecific => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + event_category_override: params.eventCategoryOverride, + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threat_filters: params.threatFilters, + threat_query: params.threatQuery, + threat_mapping: params.threatMapping, + threat_language: params.threatLanguage, + threat_index: params.threatIndex, + concurrent_searches: params.concurrentSearches, + items_per_search: params.itemsPerSearch, + }; + } + case 'query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threshold: params.threshold, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomaly_threshold: params.anomalyThreshold, + machine_learning_job_id: params.machineLearningJobId, + }; + } + default: { + return assertUnreachable(params); + } + } +}; + +export const internalRuleToAPIResponse = ( + rule: InternalRuleResponse, + ruleActions: RuleActions +): FullResponseSchema => { + return { + id: rule.id, + immutable: rule.params.immutable, + updated_at: rule.updatedAt, + updated_by: rule.updatedBy, + created_at: rule.createdAt, + created_by: rule.createdBy, + name: rule.name, + tags: rule.tags, + interval: rule.schedule.interval, + enabled: rule.enabled, + throttle: ruleActions.ruleThrottle, + actions: ruleActions.actions, + description: rule.params.description, + risk_score: rule.params.riskScore, + severity: rule.params.severity, + building_block_type: rule.params.buildingBlockType, + note: rule.params.note, + license: rule.params.license, + output_index: rule.params.outputIndex, + timeline_id: rule.params.timelineId, + timeline_title: rule.params.timelineTitle, + meta: rule.params.meta, + rule_name_override: rule.params.ruleNameOverride, + timestamp_override: rule.params.timestampOverride, + author: rule.params.author ?? [], + false_positives: rule.params.falsePositives, + from: rule.params.from, + rule_id: rule.params.ruleId, + max_signals: rule.params.maxSignals, + risk_score_mapping: rule.params.riskScoreMapping ?? [], + severity_mapping: rule.params.severityMapping ?? [], + threat: rule.params.threat, + to: rule.params.to, + references: rule.params.references, + version: rule.params.version, + exceptions_list: rule.params.exceptionsList ?? [], + ...typeSpecificCamelToSnake(rule.params), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts new file mode 100644 index 0000000000000..5bb8d6d6746f9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -0,0 +1,210 @@ +/* + * 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 * as t from 'io-ts'; + +import { listArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; +import { + threat_mapping, + threat_index, + threat_query, + concurrentSearchesOrUndefined, + itemsPerSearchOrUndefined, +} from '../../../../common/detection_engine/schemas/types/threat_mapping'; +import { + authorOrUndefined, + buildingBlockTypeOrUndefined, + description, + enabled, + noteOrUndefined, + false_positives, + from, + rule_id, + immutable, + indexOrUndefined, + licenseOrUndefined, + output_index, + timelineIdOrUndefined, + timelineTitleOrUndefined, + metaOrUndefined, + name, + query, + queryOrUndefined, + filtersOrUndefined, + machine_learning_job_id, + max_signals, + risk_score, + riskScoreMappingOrUndefined, + ruleNameOverrideOrUndefined, + severity, + severityMappingOrUndefined, + tags, + timestampOverrideOrUndefined, + threat, + to, + references, + version, + eventCategoryOverrideOrUndefined, + savedIdOrUndefined, + saved_id, + threshold, + anomaly_threshold, + actionsCamel, + throttleOrNull, + createdByOrNull, + updatedByOrNull, + created_at, + updated_at, +} from '../../../../common/detection_engine/schemas/common/schemas'; +import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants'; + +const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); +export const baseRuleParams = t.exact( + t.type({ + author: authorOrUndefined, + buildingBlockType: buildingBlockTypeOrUndefined, + description, + note: noteOrUndefined, + falsePositives: false_positives, + from, + ruleId: rule_id, + immutable, + license: licenseOrUndefined, + outputIndex: output_index, + timelineId: timelineIdOrUndefined, + timelineTitle: timelineTitleOrUndefined, + meta: metaOrUndefined, + // maxSignals not used in ML rules but probably should be used + maxSignals: max_signals, + riskScore: risk_score, + riskScoreMapping: riskScoreMappingOrUndefined, + ruleNameOverride: ruleNameOverrideOrUndefined, + severity, + severityMapping: severityMappingOrUndefined, + timestampOverride: timestampOverrideOrUndefined, + threat, + to, + references, + version, + exceptionsList: listArrayOrUndefined, + }) +); +export type BaseRuleParams = t.TypeOf; + +const eqlSpecificRuleParams = t.type({ + type: t.literal('eql'), + language: t.literal('eql'), + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + eventCategoryOverride: eventCategoryOverrideOrUndefined, +}); + +const threatSpecificRuleParams = t.type({ + type: t.literal('threat_match'), + language: nonEqlLanguages, + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + savedId: savedIdOrUndefined, + threatFilters: filtersOrUndefined, + threatQuery: threat_query, + threatMapping: threat_mapping, + threatLanguage: t.union([nonEqlLanguages, t.undefined]), + threatIndex: threat_index, + concurrentSearches: concurrentSearchesOrUndefined, + itemsPerSearch: itemsPerSearchOrUndefined, +}); + +const querySpecificRuleParams = t.exact( + t.type({ + type: t.literal('query'), + language: nonEqlLanguages, + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + savedId: savedIdOrUndefined, + }) +); + +const savedQuerySpecificRuleParams = t.type({ + type: t.literal('saved_query'), + // Having language, query, and filters possibly defined adds more code confusion and probably user confusion + // if the saved object gets deleted for some reason + language: nonEqlLanguages, + index: indexOrUndefined, + query: queryOrUndefined, + filters: filtersOrUndefined, + savedId: saved_id, +}); + +const thresholdSpecificRuleParams = t.type({ + type: t.literal('threshold'), + language: nonEqlLanguages, + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + savedId: savedIdOrUndefined, + threshold, +}); + +const machineLearningSpecificRuleParams = t.type({ + type: t.literal('machine_learning'), + anomalyThreshold: anomaly_threshold, + machineLearningJobId: machine_learning_job_id, +}); + +export const typeSpecificRuleParams = t.union([ + eqlSpecificRuleParams, + threatSpecificRuleParams, + querySpecificRuleParams, + savedQuerySpecificRuleParams, + thresholdSpecificRuleParams, + machineLearningSpecificRuleParams, +]); +export type TypeSpecificRuleParams = t.TypeOf; + +export const ruleParams = t.intersection([baseRuleParams, typeSpecificRuleParams]); +export type RuleParams = t.TypeOf; + +export const internalRuleCreate = t.type({ + name, + tags, + alertTypeId: t.literal(SIGNALS_ID), + consumer: t.literal(SERVER_APP_ID), + schedule: t.type({ + interval: t.string, + }), + enabled, + actions: actionsCamel, + params: ruleParams, + throttle: throttleOrNull, +}); +export type InternalRuleCreate = t.TypeOf; + +export const internalRuleUpdate = t.type({ + name, + tags, + schedule: t.type({ + interval: t.string, + }), + actions: actionsCamel, + params: ruleParams, + throttle: throttleOrNull, +}); +export type InternalRuleUpdate = t.TypeOf; + +export const internalRuleResponse = t.intersection([ + internalRuleCreate, + t.type({ + id: t.string, + createdBy: createdByOrNull, + updatedBy: updatedByOrNull, + createdAt: created_at, + updatedAt: updated_at, + }), +]); +export type InternalRuleResponse = t.TypeOf; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 237feb84932d2..53a8f1f4ca5c0 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -65,13 +66,51 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule without an input index', async () => { - const { index, ...payload } = getSimpleRule(); - const { index: _index, ...expected } = getSimpleRuleOutput(); + const rule: CreateRulesSchema = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + }; + const expected = { + actions: [], + author: [], + created_by: 'elastic', + description: 'Simple Rule Query', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 1, + risk_score_mapping: [], + name: 'Simple Rule Query', + query: 'user.name: root or user.name: admin', + references: [], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [], + throttle: 'no_actions', + exceptions_list: [], + version: 1, + }; const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(payload) + .send(rule) .expect(200); const bodyToCompare = removeServerGeneratedProperties(body); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts index a0fcac159a73e..2f5a043881eeb 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts @@ -19,6 +19,7 @@ import { getSimpleRuleUpdate, getSimpleMlRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -38,7 +39,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -60,7 +61,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return a 403 forbidden if it is a machine learning job', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's type to try to be a machine learning job type const updatedRule = getSimpleMlRuleUpdate('rule-1'); @@ -81,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an auto-generated rule_id', async () => { - const rule = getSimpleRuleUpdate('rule-1'); + const rule = getSimpleRule('rule-1'); delete rule.rule_id; const createRuleBody = await createRule(supertest, rule); @@ -105,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -127,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -150,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts index a846771fe7683..22aa40b0721a4 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts @@ -18,6 +18,7 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; @@ -57,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // create a second simple rule await supertest @@ -94,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an id', async () => { - const createRuleBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createRuleBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -116,8 +117,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules id', async () => { - const createRule1 = await createRule(supertest, getSimpleRuleUpdate('rule-1')); - const createRule2 = await createRule(supertest, getSimpleRuleUpdate('rule-2')); + const createRule1 = await createRule(supertest, getSimpleRule('rule-1')); + const createRule2 = await createRule(supertest, getSimpleRule('rule-2')); // update both rule names const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -196,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's timeline_title const ruleUpdate = getSimpleRuleUpdate('rule-1'); @@ -269,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.name = 'some other name'; @@ -304,7 +305,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update one rule name and give a fake id for the second const rule1 = getSimpleRuleUpdate(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index 75d5e47b7b990..d473863e7d028 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -19,7 +20,6 @@ import { waitForRuleSuccess, createRule, } from '../../utils'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index 238768e1feb8f..651a7601ca95a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -7,10 +7,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { deleteAllExceptions } from '../../../lists_api_integration/utils'; import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common'; import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; @@ -422,7 +422,14 @@ export default ({ getService }: FtrProviderContext) => { await createExceptionListItem(supertest, exceptionListItem); const ruleWithException: CreateRulesSchema = { - ...getSimpleRule(), + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + index: ['auditbeat-*'], + type: 'query', from: '1900-01-01T00:00:00.000Z', query: 'host.name: "suricata-sensor-amsterdam"', exceptions_list: [ @@ -460,9 +467,16 @@ export default ({ getService }: FtrProviderContext) => { await createExceptionListItem(supertest, exceptionListItem); const ruleWithException: CreateRulesSchema = { - ...getSimpleRule(), + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + index: ['auditbeat-*'], + type: 'query', from: '1900-01-01T00:00:00.000Z', - query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude + query: 'host.name: "suricata-sensor-amsterdam"', exceptions_list: [ { id, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 4d9bff8b0f34e..a18faf8543042 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL, @@ -110,13 +111,51 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule without an input index', async () => { - const { index, ...payload } = getSimpleRule(); - const { index: _index, ...expected } = getSimpleRuleOutput(); + const rule: CreateRulesSchema = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + }; + const expected = { + actions: [], + author: [], + created_by: 'elastic', + description: 'Simple Rule Query', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 1, + risk_score_mapping: [], + name: 'Simple Rule Query', + query: 'user.name: root or user.name: admin', + references: [], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [], + throttle: 'no_actions', + exceptions_list: [], + version: 1, + }; const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(payload) + .send(rule) .expect(200); const bodyToCompare = removeServerGeneratedProperties(body); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 498c607121760..36cd8480998c5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -23,7 +23,7 @@ import { waitForSignalsToBePresent, } from '../../utils'; -import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; // eslint-disable-next-line import/no-default-export @@ -99,7 +99,13 @@ export default ({ getService }: FtrProviderContext) => { it('should be able to execute and get 10 signals when doing a specific query', async () => { const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip @@ -127,7 +133,13 @@ export default ({ getService }: FtrProviderContext) => { it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip @@ -155,7 +167,13 @@ export default ({ getService }: FtrProviderContext) => { it('should return 0 signals when using an AND and one of the clauses does not have data', async () => { const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip @@ -187,7 +205,13 @@ export default ({ getService }: FtrProviderContext) => { it('should return 0 signals when using an AND and one of the clauses has a made up value that does not exist', async () => { const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 0cd1c21447dfe..0ba2abb466f7b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { QueryCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DEFAULT_SIGNALS_INDEX } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have recorded the rule_id within the signal', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -124,7 +124,7 @@ export default ({ getService }: FtrProviderContext) => { it('should query and get back expected signal structure when it is a signal on a signal', async () => { // create a 1 signal from 1 auditbeat record - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: CreateRulesSchema = { + const ruleForSignals: QueryCreateSchema = { ...getSimpleRule(), rule_id: 'signal-on-signal', index: [`${DEFAULT_SIGNALS_INDEX}*`], @@ -209,7 +209,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -222,7 +222,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have recorded the rule_id within the signal', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -235,7 +235,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -278,7 +278,7 @@ export default ({ getService }: FtrProviderContext) => { it('should query and get back expected signal structure when it is a signal on a signal', async () => { // create a 1 signal from 1 auditbeat record - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -288,7 +288,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: CreateRulesSchema = { + const ruleForSignals: QueryCreateSchema = { ...getSimpleRule(), rule_id: 'signal-on-signal', index: [`${DEFAULT_SIGNALS_INDEX}*`], @@ -362,7 +362,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -375,7 +375,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have recorded the rule_id within the signal', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -388,7 +388,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -437,7 +437,7 @@ export default ({ getService }: FtrProviderContext) => { it('should query and get back expected signal structure when it is a signal on a signal', async () => { // create a 1 signal from 1 auditbeat record - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -447,7 +447,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: CreateRulesSchema = { + const ruleForSignals: QueryCreateSchema = { ...getSimpleRule(), rule_id: 'signal-on-signal', index: [`${DEFAULT_SIGNALS_INDEX}*`], diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 3150e7c5b71c7..23a8776b14631 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -21,6 +21,7 @@ import { getSimpleRuleUpdate, getSimpleMlRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -40,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -84,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an auto-generated rule_id', async () => { - const rule = getSimpleRuleUpdate('rule-1'); + const rule = getSimpleRule('rule-1'); delete rule.rule_id; const createRuleBody = await createRule(supertest, rule); @@ -108,7 +109,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -130,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -153,7 +154,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index a846771fe7683..22aa40b0721a4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -18,6 +18,7 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; @@ -57,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // create a second simple rule await supertest @@ -94,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an id', async () => { - const createRuleBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createRuleBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -116,8 +117,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules id', async () => { - const createRule1 = await createRule(supertest, getSimpleRuleUpdate('rule-1')); - const createRule2 = await createRule(supertest, getSimpleRuleUpdate('rule-2')); + const createRule1 = await createRule(supertest, getSimpleRule('rule-1')); + const createRule2 = await createRule(supertest, getSimpleRule('rule-2')); // update both rule names const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -196,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's timeline_title const ruleUpdate = getSimpleRuleUpdate('rule-1'); @@ -269,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.name = 'some other name'; @@ -304,7 +305,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update one rule name and give a fake id for the second const rule1 = getSimpleRuleUpdate(); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index c5e417c710283..f458fe118dcf7 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -9,6 +9,12 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; +import { + CreateRulesSchema, + UpdateRulesSchema, + FullResponseSchema, + QueryCreateSchema, +} from '../../plugins/security_solution/common/detection_engine/schemas/request'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants'; import { CreateExceptionListItemSchema, @@ -21,8 +27,6 @@ import { Status, SignalIds, } from '../../plugins/security_solution/common/detection_engine/schemas/common/schemas'; -import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; -import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; import { DETECTION_ENGINE_INDEX_URL, @@ -37,8 +41,8 @@ import { * @param rule Rule to pass in to remove typical server generated properties */ export const removeServerGeneratedProperties = ( - rule: Partial -): Partial => { + rule: FullResponseSchema +): Partial => { const { /* eslint-disable @typescript-eslint/naming-convention */ created_at, @@ -61,8 +65,8 @@ export const removeServerGeneratedProperties = ( * @param rule Rule to pass in to remove typical server generated properties */ export const removeServerGeneratedPropertiesIncludingRuleId = ( - rule: Partial -): Partial => { + rule: FullResponseSchema +): Partial => { const ruleWithRemovedProperties = removeServerGeneratedProperties(rule); // eslint-disable-next-line @typescript-eslint/naming-convention const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties; @@ -74,7 +78,7 @@ export const removeServerGeneratedPropertiesIncludingRuleId = ( * @param ruleId * @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import */ -export const getSimpleRule = (ruleId = 'rule-1', enabled = true): CreateRulesSchema => ({ +export const getSimpleRule = (ruleId = 'rule-1', enabled = true): QueryCreateSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', enabled, @@ -384,7 +388,7 @@ export const getSimpleRuleAsNdjson = (ruleIds: string[], enabled = false): Buffe * testing upload features. * @param rule The rule to convert to ndjson */ -export const ruleToNdjson = (rule: Partial): Buffer => { +export const ruleToNdjson = (rule: CreateRulesSchema): Buffer => { const stringified = JSON.stringify(rule); return Buffer.from(`${stringified}\n`); }; @@ -725,7 +729,7 @@ export const countDownTest = async ( export const createRule = async ( supertest: SuperTest, rule: CreateRulesSchema -): Promise => { +): Promise => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true')