diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 14ae030c63df3..7f086647bbc75 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; -import { DefaultStringArray, NonEmptyString } from '../types'; import { DefaultNamespace } from '../types/default_namespace'; +import { DefaultStringArray, NonEmptyString } from '../../siem_common_deps'; export const name = t.string; export type Name = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index c10d441d93aa5..4322ff4c2801b 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -24,8 +24,9 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; -import { DefaultEntryArray, DefaultUuid } from '../types'; +import { DefaultEntryArray } from '../types'; import { EntriesArray } from '../types/entries'; +import { DefaultUuid } from '../../siem_common_deps'; export const createExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 3da8bfca126ae..a0aaa91c81427 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -22,7 +22,7 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; -import { DefaultUuid } from '../types/default_uuid'; +import { DefaultUuid } from '../../siem_common_deps'; export const createExceptionListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts index 94299c93b29d8..0a5e861d84483 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -6,6 +6,7 @@ /* eslint-disable @typescript-eslint/camelcase */ +// TODO: You cannot import a stream from common into the front end code! CHANGE THIS import { Readable } from 'stream'; import * as t from 'io-ts'; @@ -20,6 +21,7 @@ export const importListItemSchema = t.exact( export type ImportListItemSchema = t.TypeOf; +// TODO: You cannot import a stream from common into the front end code! CHANGE THIS export interface HapiReadableStream extends Readable { hapi: { filename: string; diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 674d7c40c2970..2f38ff86d4fb2 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export * from './default_entries_array'; -export * from './default_string_array'; -export * from './default_uuid'; export * from './entries'; -export * from './non_empty_string'; diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts index 9de40e3f72932..3759305987f79 100644 --- a/x-pack/plugins/lists/common/siem_common_deps.ts +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -4,5 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export { NonEmptyString } from '../../security_solution/common/detection_engine/schemas/types/non_empty_string'; +export { DefaultUuid } from '../../security_solution/common/detection_engine/schemas/types/default_uuid'; +export { DefaultStringArray } from '../../security_solution/common/detection_engine/schemas/types/default_string_array'; export { exactCheck } from '../../security_solution/common/exact_check'; export { getPaths, foldLeftRight } from '../../security_solution/common/test_utils'; 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 9eb2d9abccbd3..7db8e57421d02 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 @@ -14,16 +14,35 @@ import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greate import { PositiveInteger } from '../types/positive_integer'; export const description = t.string; +export type Description = t.TypeOf; + +export const descriptionOrUndefined = t.union([description, t.undefined]); +export type DescriptionOrUndefined = t.TypeOf; + export const enabled = t.boolean; -export const exclude_export_details = t.boolean; +export type Enabled = t.TypeOf; + +export const enabledOrUndefined = t.union([enabled, t.undefined]); +export type EnabledOrUndefined = t.TypeOf; + export const false_positives = t.array(t.string); +export type FalsePositives = t.TypeOf; + +export const falsePositivesOrUndefined = t.union([false_positives, t.undefined]); +export type FalsePositivesOrUndefined = t.TypeOf; + export const file_name = t.string; +export type FileName = t.TypeOf; + +export const exclude_export_details = t.boolean; +export type ExcludeExportDetails = t.TypeOf; /** * TODO: Right now the filters is an "unknown", when it could more than likely * become the actual ESFilter as a type. */ 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 /** * Params is an "object", since it is a type of AlertActionParams which is action templates. @@ -43,30 +62,98 @@ export const action = t.exact( ); export const actions = t.array(action); +export type Actions = t.TypeOf; // TODO: Create a regular expression type or custom date math part type here export const from = t.string; +export type From = t.TypeOf; + +export const fromOrUndefined = t.union([from, t.undefined]); +export type FromOrUndefined = t.TypeOf; export const immutable = t.boolean; +export type Immutable = t.TypeOf; // Note: Never make this a strict uuid, we allow the rule_id to be any string at the moment // in case we encounter 3rd party rule systems which might be using auto incrementing numbers // or other different things. export const rule_id = t.string; +export type RuleId = t.TypeOf; + +export const ruleIdOrUndefined = t.union([rule_id, t.undefined]); +export type RuleIdOrUndefined = t.TypeOf; export const id = UUID; +export const idOrUndefined = t.union([id, t.undefined]); +export type IdOrUndefined = t.TypeOf; + export const index = t.array(t.string); +export type Index = t.TypeOf; + +export const indexOrUndefined = t.union([index, t.undefined]); +export type IndexOrUndefined = t.TypeOf; + export const interval = t.string; +export type Interval = t.TypeOf; + +export const intervalOrUndefined = t.union([interval, t.undefined]); +export type IntervalOrUndefined = t.TypeOf; + export const query = t.string; +export type Query = t.TypeOf; + +export const queryOrUndefined = t.union([query, t.undefined]); +export type QueryOrUndefined = t.TypeOf; + export const language = t.keyof({ kuery: null, lucene: null }); +export type Language = t.TypeOf; + +export const languageOrUndefined = t.union([language, t.undefined]); +export type LanguageOrUndefined = t.TypeOf; + export const objects = t.array(t.type({ rule_id })); + export const output_index = t.string; +export type OutputIndex = t.TypeOf; + +export const outputIndexOrUndefined = t.union([output_index, t.undefined]); +export type OutputIndexOrUndefined = t.TypeOf; + export const saved_id = t.string; +export type SavedId = t.TypeOf; + +export const savedIdOrUndefined = t.union([saved_id, t.undefined]); +export type SavedIdOrUndefined = t.TypeOf; + export const timeline_id = t.string; +export type TimelineId = t.TypeOf; + +export const timelineIdOrUndefined = t.union([timeline_id, t.undefined]); +export type TimelineIdOrUndefined = t.TypeOf; + export const timeline_title = t.string; +export type TimelineTitle = t.TypeOf; + +export const timelineTitleOrUndefined = t.union([timeline_title, t.undefined]); +export type TimelineTitleOrUndefined = t.TypeOf; + export const throttle = t.string; +export type Throttle = t.TypeOf; + +export const throttleOrNull = t.union([throttle, t.null]); +export type ThrottleOrNull = t.TypeOf; + export const anomaly_threshold = PositiveInteger; +export type AnomalyThreshold = t.TypeOf; + +export const anomalyThresholdOrUndefined = t.union([anomaly_threshold, t.undefined]); +export type AnomalyThresholdOrUndefined = t.TypeOf; + export const machine_learning_job_id = t.string; +export type MachineLearningJobId = t.TypeOf; + +export const machineLearningJobIdOrUndefined = t.union([machine_learning_job_id, t.undefined]); +export type MachineLearningJobIdOrUndefined = t.TypeOf; /** * Note that this is a plain unknown object because we allow the UI @@ -76,30 +163,103 @@ export const machine_learning_job_id = t.string; * so we have tighter control over 3rd party data structures. */ export const meta = t.object; +export type Meta = t.TypeOf; +export const metaOrUndefined = t.union([meta, t.undefined]); +export type MetaOrUndefined = t.TypeOf; + export const max_signals = PositiveIntegerGreaterThanZero; +export type MaxSignals = t.TypeOf; + +export const maxSignalsOrUndefined = t.union([max_signals, t.undefined]); +export type MaxSignalsOrUndefined = t.TypeOf; + export const name = t.string; +export type Name = t.TypeOf; + +export const nameOrUndefined = t.union([name, t.undefined]); +export type NameOrUndefined = t.TypeOf; + export const risk_score = RiskScore; +export type RiskScore = t.TypeOf; + +export const riskScoreOrUndefined = t.union([risk_score, t.undefined]); +export type RiskScoreOrUndefined = t.TypeOf; + export const severity = t.keyof({ low: null, medium: null, high: null, critical: null }); +export type Severity = t.TypeOf; + +export const severityOrUndefined = t.union([severity, t.undefined]); +export type SeverityOrUndefined = t.TypeOf; + export const status = t.keyof({ open: null, closed: null }); + export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null }); // TODO: Create a regular expression type or custom date math part type here export const to = t.string; +export type To = t.TypeOf; + +export const toOrUndefined = t.union([to, t.undefined]); +export type ToOrUndefined = t.TypeOf; export const type = t.keyof({ machine_learning: null, query: null, saved_query: null }); +export type Type = t.TypeOf; + +export const typeOrUndefined = t.union([type, t.undefined]); +export type TypeOrUndefined = t.TypeOf; + export const queryFilter = t.string; +export type QueryFilter = t.TypeOf; + +export const queryFilterOrUndefined = t.union([queryFilter, t.undefined]); +export type QueryFilterOrUndefined = t.TypeOf; + export const references = t.array(t.string); +export type References = t.TypeOf; + +export const referencesOrUndefined = t.union([references, t.undefined]); +export type ReferencesOrUndefined = t.TypeOf; + export const per_page = PositiveInteger; +export type PerPage = t.TypeOf; + +export const perPageOrUndefined = t.union([per_page, t.undefined]); +export type PerPageOrUndefined = t.TypeOf; + export const page = PositiveIntegerGreaterThanZero; +export type Page = t.TypeOf; + +export const pageOrUndefined = t.union([page, t.undefined]); +export type PageOrUndefined = t.TypeOf; + export const signal_ids = t.array(t.string); // TODO: Can this be more strict or is this is the set of all Elastic Queries? export const signal_status_query = t.object; export const sort_field = t.string; +export type SortField = t.TypeOf; + +export const sortFieldOrUndefined = t.union([sort_field, t.undefined]); +export type SortFieldOrUndefined = t.TypeOf; + export const sort_order = t.keyof({ asc: null, desc: null }); +export type sortOrder = t.TypeOf; + +export const sortOrderOrUndefined = t.union([sort_order, t.undefined]); +export type SortOrderOrUndefined = t.TypeOf; + export const tags = t.array(t.string); +export type Tags = t.TypeOf; + +export const tagsOrUndefined = t.union([tags, t.undefined]); +export type TagsOrUndefined = t.TypeOf; + export const fields = t.array(t.string); +export type Fields = t.TypeOf; +export const fieldsOrUndefined = t.union([fields, t.undefined]); +export type FieldsOrUndefined = t.TypeOf; + export const threat_framework = t.string; export const threat_tactic_id = t.string; export const threat_tactic_name = t.string; @@ -129,11 +289,23 @@ export const threat = t.array( }) ) ); + +export type Threat = t.TypeOf; + +export const threatOrUndefined = t.union([threat, t.undefined]); +export type ThreatOrUndefined = t.TypeOf; + export const created_at = IsoDateString; export const updated_at = IsoDateString; export const updated_by = t.string; export const created_by = t.string; + export const version = PositiveIntegerGreaterThanZero; +export type Version = t.TypeOf; + +export const versionOrUndefined = t.union([version, t.undefined]); +export type VersionOrUndefined = t.TypeOf; + export const last_success_at = IsoDateString; export const last_success_message = t.string; export const last_failure_at = IsoDateString; @@ -150,7 +322,12 @@ export const success_count = PositiveInteger; export const rules_custom_installed = PositiveInteger; export const rules_not_installed = PositiveInteger; export const rules_not_updated = PositiveInteger; + export const note = t.string; +export type Note = t.TypeOf; + +export const noteOrUndefined = t.union([note, t.undefined]); +export type NoteOrUndefined = t.TypeOf; // NOTE: Experimental list support not being shipped currently and behind a feature flag // TODO: Remove this comment once we lists have passed testing and is ready for the release @@ -185,3 +362,6 @@ export const list_and = t.intersection([ and: t.array(list), }), ]); + +export const listAndOrUndefined = t.union([t.array(list_and), t.undefined]); +export type ListAndOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts new file mode 100644 index 0000000000000..52a210f3a01aa --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts @@ -0,0 +1,47 @@ +/* + * 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 { + AddPrepackagedRulesSchema, + AddPrepackagedRulesSchemaDecoded, +} from './add_prepackaged_rules_schema'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +export const getAddPrepackagedRulesSchemaMock = (): AddPrepackagedRulesSchema => ({ + 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', + version: 1, +}); + +export const getAddPrepackagedRulesSchemaDecodedMock = (): AddPrepackagedRulesSchemaDecoded => ({ + 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', + references: [], + actions: [], + enabled: false, + 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', +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts new file mode 100644 index 0000000000000..3e7e7e5409c9c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + description, + anomaly_threshold, + filters, + index, + saved_id, + timeline_id, + timeline_title, + meta, + machine_learning_job_id, + risk_score, + MaxSignals, + name, + severity, + Tags, + To, + type, + Threat, + ThrottleOrNull, + note, + References, + Actions, + Enabled, + FalsePositives, + From, + Interval, + language, + query, + rule_id, + version, +} from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DefaultStringArray } from '../types/default_string_array'; +import { DefaultActionsArray } from '../types/default_actions_array'; +import { DefaultBooleanFalse } from '../types/default_boolean_false'; +import { DefaultFromString } from '../types/default_from_string'; +import { DefaultIntervalString } from '../types/default_interval_string'; +import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; +import { DefaultToString } from '../types/default_to_string'; +import { DefaultThreatArray } from '../types/default_threat_array'; +import { DefaultThrottleNull } from '../types/default_throttle_null'; +import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; + +/** + * Big differences between this schema and the createRulesSchema + * - rule_id is required here + * - output_index is not allowed (and instead the space index must be used) + * - immutable is forbidden but defaults to true instead of to false and it can only ever be true (This is forced directly in the route and not here) + * - enabled defaults to false instead of true + * - version is a required field that must exist + * - index is a required field that must exist if type !== machine_learning (Checked within the runtime type dependent system) + */ +export const addPrepackagedRulesSchema = t.intersection([ + t.exact( + t.type({ + description, + risk_score, + name, + severity, + type, + rule_id, + version, + }) + ), + 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 + enabled: DefaultBooleanFalse, // defaults to false 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 + 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 + 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 + 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 + throttle: DefaultThrottleNull, // defaults to "null" 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 + exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + }) + ), +]); + +export type AddPrepackagedRulesSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type AddPrepackagedRulesSchemaDecoded = Omit< + AddPrepackagedRulesSchema, + | 'references' + | 'actions' + | 'enabled' + | 'false_positives' + | 'from' + | 'interval' + | 'max_signals' + | 'tags' + | 'to' + | 'threat' + | 'throttle' + | 'exceptions_list' +> & { + references: References; + actions: Actions; + enabled: Enabled; + false_positives: FalsePositives; + from: From; + interval: Interval; + max_signals: MaxSignals; + tags: Tags; + to: To; + threat: Threat; + throttle: ThrottleOrNull; + exceptions_list: ListsDefaultArraySchema; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts new file mode 100644 index 0000000000000..793d4b04ed0e5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { AddPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; +import { addPrepackagedRuleValidateTypeDependents } from './add_prepackaged_rules_type_dependents'; +import { getAddPrepackagedRulesSchemaMock } from './add_prepackaged_rules_schema.mock'; + +describe('create_rules_type_dependents', () => { + test('saved_id is required when type is saved_query and will not validate without out', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + type: 'saved_query', + }; + delete schema.saved_id; + const errors = addPrepackagedRuleValidateTypeDependents(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: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + type: 'saved_query', + saved_id: '123', + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + timeline_id: '123', + }; + delete schema.timeline_title; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + timeline_id: '123', + timeline_title: '', + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_title" cannot be an empty string']); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + timeline_id: '', + timeline_title: 'some-title', + }; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_id" cannot be an empty string']); + }); + + test('You cannot have timeline_title without timeline_id', () => { + const schema: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + timeline_title: 'some-title', + }; + delete schema.timeline_id; + const errors = addPrepackagedRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts new file mode 100644 index 0000000000000..2788c331154d2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_type_dependents.ts @@ -0,0 +1,107 @@ +/* + * 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 { AddPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; + +export const validateAnomalyThreshold = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.anomaly_threshold == null) { + return ['when "type" is "machine_learning" anomaly_threshold is required']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateQuery = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.query != null) { + return ['when "type" is "machine_learning", "query" cannot be set']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateLanguage = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.language != null) { + return ['when "type" is "machine_learning", "language" cannot be set']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateSavedId = (rule: AddPrepackagedRulesSchema): 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: AddPrepackagedRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.machine_learning_job_id == null) { + return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateTimelineId = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.timeline_id != null) { + if (rule.timeline_title == null) { + return ['when "timeline_id" exists, "timeline_title" must also exist']; + } else if (rule.timeline_id === '') { + return ['"timeline_id" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateTimelineTitle = (rule: AddPrepackagedRulesSchema): string[] => { + if (rule.timeline_title != null) { + if (rule.timeline_id == null) { + return ['when "timeline_title" exists, "timeline_id" must also exist']; + } else if (rule.timeline_title === '') { + return ['"timeline_title" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const addPrepackagedRuleValidateTypeDependents = ( + schema: AddPrepackagedRulesSchema +): string[] => { + return [ + ...validateAnomalyThreshold(schema), + ...validateQuery(schema), + ...validateLanguage(schema), + ...validateSavedId(schema), + ...validateMachineLearningJobId(schema), + ...validateTimelineId(schema), + ...validateTimelineTitle(schema), + ]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts new file mode 100644 index 0000000000000..5d170f5a78645 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -0,0 +1,1389 @@ +/* + * 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 { + addPrepackagedRulesSchema, + AddPrepackagedRulesSchemaDecoded, + AddPrepackagedRulesSchema, +} from './add_prepackaged_rules_schema'; + +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 { + getAddPrepackagedRulesSchemaMock, + getAddPrepackagedRulesSchemaDecodedMock, +} from './add_prepackaged_rules_schema.mock'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +describe('add prepackaged rules schema', () => { + test('empty objects do not validate', () => { + const payload: Partial = {}; + + const decoded = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "rule_id"', + 'Invalid value "undefined" supplied to "version"', + ]); + expect(message.schema).toEqual({}); + }); + + test('made up values do not validate', () => { + const payload: AddPrepackagedRulesSchema & { madeUp: string } = { + ...getAddPrepackagedRulesSchemaMock(), + madeUp: 'hi', + }; + + const decoded = addPrepackagedRulesSchema.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 = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "version"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description] does not validate', () => { + const payload: Partial = { + rule_id: 'rule-1', + description: 'some description', + }; + + const decoded = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "version"', + ]); + 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 = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "version"', + ]); + 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 = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "version"', + ]); + 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 = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "version"', + ]); + 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 = addPrepackagedRulesSchema.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"', + 'Invalid value "undefined" supplied to "version"', + ]); + 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 = addPrepackagedRulesSchema.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 "version"', + ]); + 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 = addPrepackagedRulesSchema.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 "version"', + ]); + 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 = addPrepackagedRulesSchema.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 "version"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity, type, query, index, interval, version] does validate', () => { + const payload: AddPrepackagedRulesSchema = { + 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', + version: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + 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: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + 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', + risk_score: 50, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "version"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, version] does 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', + risk_score: 50, + version: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + 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: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + 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 not validate', () => { + const payload: Partial & { output_index: string } = { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "version"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index, version] does not validate because output_index is not allowed', () => { + const payload: Partial & { output_index: string } = { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + version: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "output_index"']); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version] does 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', + risk_score: 50, + version: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + 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, + version: 1, + actions: [], + enabled: false, + exceptions_list: [], + false_positives: [], + max_signals: 100, + references: [], + tags: [], + threat: [], + throttle: null, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can send in an empty array to threat', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + threat: [], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + 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: AddPrepackagedRulesSchema = { + 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', + threat: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + version: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + 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', + threat: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + throttle: null, + version: 1, + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('allows references to be sent as valid', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + references: ['index-1'], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + references: ['index-1'], + }; + expect(message.schema).toEqual(expected); + }); + + test('defaults references to an array if it is not sent in', () => { + const { references, ...noReferences } = getAddPrepackagedRulesSchemaMock(); + const decoded = addPrepackagedRulesSchema.decode(noReferences); + const checked = exactCheck(noReferences, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + references: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('immutable cannot be set in a pre-packaged rule', () => { + const payload: AddPrepackagedRulesSchema & { immutable: boolean } = { + ...getAddPrepackagedRulesSchemaMock(), + immutable: true, + }; + + const decoded = addPrepackagedRulesSchema.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('defaults enabled to false', () => { + const payload: AddPrepackagedRulesSchema = getAddPrepackagedRulesSchemaMock(); + delete payload.enabled; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(((message.schema as unknown) as AddPrepackagedRulesSchemaDecoded).enabled).toEqual( + false + ); + }); + + test('rule_id is required', () => { + const payload: AddPrepackagedRulesSchema = getAddPrepackagedRulesSchemaMock(); + delete payload.rule_id; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "rule_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('references cannot be numbers', () => { + const payload: Omit & { references: number[] } = { + ...getAddPrepackagedRulesSchemaMock(), + references: [5], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('indexes cannot be numbers', () => { + const payload: Omit & { index: number[] } = { + ...getAddPrepackagedRulesSchemaMock(), + index: [5], + }; + + const decoded = addPrepackagedRulesSchema.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 } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noInterval, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { + interval: expectedInterval, + ...expectedNoInterval + } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...expectedNoInterval, + interval: '5m', + }; + expect(message.schema).toEqual(expected); + }); + + test('defaults max signals to 100', () => { + const { max_signals, ...noMaxSignals } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noMaxSignals, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { + max_signals: expectedMaxSignals, + ...expectedNoMaxSignals + } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...expectedNoMaxSignals, + max_signals: 100, + }; + expect(message.schema).toEqual(expected); + }); + + test('saved_query type can have filters with it', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + filters: [], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('filters cannot be a string', () => { + const payload: Omit & { filters: string } = { + ...getAddPrepackagedRulesSchemaMock(), + filters: 'some string', + }; + + const decoded = addPrepackagedRulesSchema.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: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + language: 'kuery', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + language: 'kuery', + }; + expect(message.schema).toEqual(expected); + }); + + test('language validates with lucene', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + language: 'lucene', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + language: 'lucene', + }; + expect(message.schema).toEqual(expected); + }); + + test('language does not validate with something made up', () => { + const payload: Omit & { language: string } = { + ...getAddPrepackagedRulesSchemaMock(), + language: 'something-made-up', + }; + + const decoded = addPrepackagedRulesSchema.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: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + max_signals: -1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('max_signals cannot be zero', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + max_signals: 0, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('max_signals can be 1', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + max_signals: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + max_signals: 1, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can optionally send in an array of tags', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + tags: ['tag_1', 'tag_2'], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + 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[] } = { + ...getAddPrepackagedRulesSchemaMock(), + tags: [0, 1, 2], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "2" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "framework"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getAddPrepackagedRulesSchemaMock(), + threat: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "framework"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "tactic"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getAddPrepackagedRulesSchemaMock(), + threat: [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "tactic"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "technique"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getAddPrepackagedRulesSchemaMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "technique"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You can optionally send in an array of false positives', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + false_positives: ['false_1', 'false_2'], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + 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[]; + } = { + ...getAddPrepackagedRulesSchemaMock(), + false_positives: [5, 4], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to ""', + 'Invalid value "4" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + test('You cannot set the risk_score to 101', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + risk_score: 101, + }; + + const decoded = addPrepackagedRulesSchema.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: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + risk_score: -1, + }; + + const decoded = addPrepackagedRulesSchema.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: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + risk_score: 0, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + risk_score: 0, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can set the risk_score to 100', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + risk_score: 100, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + risk_score: 100, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can set meta to any object you want', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot create meta as a string', () => { + const payload: Omit & { meta: string } = { + ...getAddPrepackagedRulesSchemaMock(), + meta: 'should not work', + }; + + const decoded = addPrepackagedRulesSchema.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 } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noQuery, + filters: [], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { query: expectedQuery, ...expectedNoQuery } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...expectedNoQuery, + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('validates with timeline_id and timeline_title', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + 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 } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noFrom, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { from: expectedFrom, ...expectedNoFrom } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...expectedNoFrom, + from: 'now-6m', + }; + expect(message.schema).toEqual(expected); + }); + + test('The default for "to" will be "now"', () => { + const { to, ...noTo } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noTo, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { to: expectedTo, ...expectedNoTo } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...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 } = { + ...getAddPrepackagedRulesSchemaMock(), + severity: 'junk', + }; + + const decoded = addPrepackagedRulesSchema.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 } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noActions, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { + actions: expectedActions, + ...expectedNoActions + } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...expectedNoActions, + actions: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + const payload: Omit = { + ...getAddPrepackagedRulesSchemaMock(), + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "group"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + const payload: Omit = { + ...getAddPrepackagedRulesSchemaMock(), + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + const payload: Omit = { + ...getAddPrepackagedRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', params: {} }], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + const payload: Omit = { + ...getAddPrepackagedRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "params"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are including "actionTypeId"', () => { + const payload: Omit = { + ...getAddPrepackagedRulesSchemaMock(), + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('The default for "throttle" will be null', () => { + const { throttle, ...noThrottle } = getAddPrepackagedRulesSchemaMock(); + const payload: AddPrepackagedRulesSchema = { + ...noThrottle, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { + throttle: expectedThrottle, + ...expectedNoThrottle + } = getAddPrepackagedRulesSchemaDecodedMock(); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...expectedNoThrottle, + throttle: null, + }; + expect(message.schema).toEqual(expected); + }); + + describe('note', () => { + test('You can set note to a string', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + note: '# documentation markdown here', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + note: '# documentation markdown here', + }; + expect(message.schema).toEqual(expected); + }); + + test('You can set note to an empty string', () => { + const payload: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaMock(), + note: '', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchema = { + ...getAddPrepackagedRulesSchemaDecodedMock(), + note: '', + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot create note as an object', () => { + const payload: Omit & { note: {} } = { + ...getAddPrepackagedRulesSchemaMock(), + note: { + somethingHere: 'something else', + }, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + // TODO: Fix/Change the formatErrors to be better able to handle objects + 'Invalid value "[object Object]" 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: AddPrepackagedRulesSchema = { + 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', + version: 1, + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + 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: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + }); + + // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin + describe.skip('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + + test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + }); +}); 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 new file mode 100644 index 0000000000000..e79dde41752a3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts @@ -0,0 +1,281 @@ +/* + * 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 { + createRulesBulkSchema, + CreateRulesBulkSchema, + CreateRulesBulkSchemaDecoded, +} 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'; + +// only the basics of testing are here. +// see: create_rules_schema.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', () => { + const payload: CreateRulesBulkSchema = []; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(output.errors).toEqual([]); + expect(output.schema).toEqual([]); + }); + + test('made up values do not validate for a single element', () => { + const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }]; + + 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(output.schema).toEqual({}); + }); + + test('single array element does validate', () => { + const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock()]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([getCreateRulesSchemaDecodedMock()]); + }); + + test('two array elements do validate', () => { + const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([ + getCreateRulesSchemaDecodedMock(), + getCreateRulesSchemaDecodedMock(), + ]); + }); + + test('single array element with a missing value (risk_score) will not validate', () => { + const singleItem = getCreateRulesSchemaMock(); + delete singleItem.risk_score; + const payload: CreateRulesBulkSchema = [singleItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => { + const singleItem = getCreateRulesSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); + delete secondItem.risk_score; + const payload: CreateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => { + const singleItem = getCreateRulesSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); + delete singleItem.risk_score; + const payload: CreateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where both are invalid (risk_score) will not validate', () => { + const singleItem = getCreateRulesSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); + delete singleItem.risk_score; + delete secondItem.risk_score; + const payload: CreateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + 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 } = { + ...getCreateRulesSchemaMock(), + madeUpValue: 'something', + }; + const secondItem = getCreateRulesSchemaMock(); + const payload: CreateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']); + expect(output.schema).toEqual({}); + }); + + 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 } = { + ...getCreateRulesSchemaMock(), + madeUpValue: 'something', + }; + const payload: CreateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']); + expect(output.schema).toEqual({}); + }); + + test('two array elements where both are invalid (extra key and value) will not validate', () => { + const singleItem: CreateRulesSchema & { madeUpValue: string } = { + ...getCreateRulesSchemaMock(), + madeUpValue: 'something', + }; + const secondItem: CreateRulesSchema & { madeUpValue: string } = { + ...getCreateRulesSchemaMock(), + madeUpValue: 'something', + }; + const payload: CreateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue,madeUpValue"']); + 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]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['Invalid value "madeup" supplied to "severity"']); + expect(output.schema).toEqual({}); + }); + + test('You can set "note" to a string', () => { + const payload: CreateRulesBulkSchema = [ + { ...getCreateRulesSchemaMock(), note: '# test markdown' }, + ]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([ + { ...getCreateRulesSchemaDecodedMock(), note: '# test markdown' }, + ]); + }); + + test('You can set "note" to an empty string', () => { + const payload: CreateRulesBulkSchema = [{ ...getCreateRulesSchemaMock(), note: '' }]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([{ ...getCreateRulesSchemaDecodedMock(), note: '' }]); + }); + + test('You can set "note" to anything other than string', () => { + const payload = [ + { + ...getCreateRulesSchemaMock(), + note: { + something: 'some object', + }, + }, + ]; + + const decoded = createRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + // TODO: We should change the formatter used to better print objects + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "[object Object]" supplied to "note"', + ]); + 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 new file mode 100644 index 0000000000000..c6233cc63fa9f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { createRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema'; + +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 new file mode 100644 index 0000000000000..a9ab6f8959e24 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts @@ -0,0 +1,43 @@ +/* + * 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 = (): CreateRulesSchema => ({ + 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 getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ + 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', + 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', +}); 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/create_rules_schema.test.ts new file mode 100644 index 0000000000000..d672d38028902 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts @@ -0,0 +1,1445 @@ +/* + * 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, + CreateRulesSchema, + CreateRulesSchemaDecoded, +} from './create_rules_schema'; +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, +} from './create_rules_schema.mock'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +describe('create rules schema', () => { + test('empty objects do not validate', () => { + const payload: Partial = {}; + + 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.schema).toEqual({}); + }); + + test('made up values do not validate', () => { + const payload: CreateRulesSchema & { madeUp: string } = { + ...getCreateRulesSchemaMock(), + madeUp: 'hi', + }; + + const decoded = createRulesSchema.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 = 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.schema).toEqual({}); + }); + + test('[rule_id, description] does not validate', () => { + const payload: Partial = { + rule_id: 'rule-1', + description: 'some description', + }; + + 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.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 = 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.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 = 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.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 = 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.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 = 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.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 = 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"', + ]); + 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 = 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"', + ]); + 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 = 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"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { + const payload: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + 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 = 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"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { + const payload: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { + const payload: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { + const payload: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { + const payload: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + test('You can send in an empty array to threat', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + threat: [], + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { + const payload: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + test('allows references to be sent as valid', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + references: ['index-1'], + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('references cannot be numbers', () => { + const payload: Omit & { references: number[] } = { + ...getCreateRulesSchemaMock(), + references: [5], + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('indexes cannot be numbers', () => { + const payload: Omit & { index: number[] } = { + ...getCreateRulesSchemaMock(), + index: [5], + }; + + const decoded = createRulesSchema.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('saved_query type can have filters with it', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + filters: [], + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('filters cannot be a string', () => { + const payload: Omit & { filters: string } = { + ...getCreateRulesSchemaMock(), + filters: 'some string', + }; + + const decoded = createRulesSchema.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: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + language: 'kuery', + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('language validates with lucene', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + language: 'lucene', + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('language does not validate with something made up', () => { + const payload: Omit & { language: string } = { + ...getCreateRulesSchemaMock(), + language: 'something-made-up', + }; + + const decoded = createRulesSchema.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: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + max_signals: -1, + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('max_signals cannot be zero', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + max_signals: 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 ""']); + expect(message.schema).toEqual({}); + }); + + test('max_signals can be 1', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + max_signals: 1, + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You can optionally send in an array of tags', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + tags: ['tag_1', 'tag_2'], + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You cannot send in an array of tags that are numbers', () => { + const payload: Omit & { tags: number[] } = { + ...getCreateRulesSchemaMock(), + tags: [0, 1, 2], + }; + + 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 ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "2" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "framework"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getCreateRulesSchemaMock(), + threat: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + 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 "framework"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "tactic"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getCreateRulesSchemaMock(), + threat: [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + 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 "tactic"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "technique"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getCreateRulesSchemaMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + + 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 "technique"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You can optionally send in an array of false positives', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + false_positives: ['false_1', 'false_2'], + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You cannot send in an array of false positives that are numbers', () => { + const payload: Omit & { false_positives: number[] } = { + ...getCreateRulesSchemaMock(), + false_positives: [5, 4], + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to ""', + 'Invalid value "4" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the immutable to a number when trying to create a rule', () => { + const payload: Omit & { immutable: number } = { + ...getCreateRulesSchemaMock(), + immutable: 5, + }; + + const decoded = createRulesSchema.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: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + risk_score: 101, + }; + + const decoded = createRulesSchema.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: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + risk_score: -1, + }; + + const decoded = createRulesSchema.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: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + risk_score: 0, + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You can set the risk_score to 100', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + risk_score: 100, + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You can set meta to any object you want', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You cannot create meta as a string', () => { + const payload: Omit & { meta: string } = { + ...getCreateRulesSchemaMock(), + meta: 'should not work', + }; + + const decoded = createRulesSchema.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 } = getCreateRulesSchemaMock(); + const payload: CreateRulesSchema = { + ...noQuery, + filters: [], + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('validates with timeline_id and timeline_title', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You cannot set the severity to a value other than low, medium, high, or critical', () => { + const payload: Omit & { severity: string } = { + ...getCreateRulesSchemaMock(), + severity: 'junk', + }; + + const decoded = createRulesSchema.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('You cannot send in an array of actions that are missing "group"', () => { + const payload: Omit = { + ...getCreateRulesSchemaMock(), + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + }; + + 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 "group"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + const payload: Omit = { + ...getCreateRulesSchemaMock(), + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + }; + + 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 "id"']); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + const payload: Omit = { + ...getCreateRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', params: {} }], + }; + + 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 "action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + const payload: Omit = { + ...getCreateRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + }; + + 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 "params"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are including "actionTypeId"', () => { + const payload: Omit = { + ...getCreateRulesSchemaMock(), + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + + 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 "action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + describe('note', () => { + test('You can set note to a string', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + note: '# documentation markdown here', + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You can set note to an empty string', () => { + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + note: '', + }; + + const decoded = createRulesSchema.decode(payload); + 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); + }); + + test('You cannot create note as an object', () => { + const payload: Omit & { note: {} } = { + ...getCreateRulesSchemaMock(), + note: { + somethingHere: 'something else', + }, + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + // TODO: Fix/Change the formatErrors to be better able to handle objects + 'Invalid value "[object Object]" 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: CreateRulesSchema = { + 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 = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + }); + + 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', () => { + 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', + 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', + rule_id: 'rule-1', + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + 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); + }); + + test('it generates a uuid v4 whenever you omit the rule_id', () => { + const { rule_id, ...noRuleId } = getCreateRulesSchemaMock(); + const decoded = createRulesSchema.decode(noRuleId); + const checked = exactCheck(noRuleId, 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 + ); + }); + + // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin + describe.skip('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + + test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + }); +}); 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 new file mode 100644 index 0000000000000..4e60201b8030e --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + description, + anomaly_threshold, + 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, + ThrottleOrNull, + note, + Version, + References, + Actions, + Enabled, + FalsePositives, + From, + Interval, + language, + query, +} from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DefaultStringArray } from '../types/default_string_array'; +import { DefaultActionsArray } from '../types/default_actions_array'; +import { DefaultBooleanTrue } from '../types/default_boolean_true'; +import { DefaultFromString } from '../types/default_from_string'; +import { DefaultIntervalString } from '../types/default_interval_string'; +import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; +import { DefaultToString } from '../types/default_to_string'; +import { DefaultThreatArray } from '../types/default_threat_array'; +import { DefaultThrottleNull } from '../types/default_throttle_null'; +import { DefaultVersionNumber } from '../types/default_version_number'; +import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; +import { DefaultUuid } from '../types/default_uuid'; + +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 + enabled: DefaultBooleanTrue, // defaults to true 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 + // 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 + 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 + throttle: DefaultThrottleNull, // defaults to "null" 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: ListsDefaultArray, // defaults to empty array 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, + | 'references' + | 'actions' + | 'enabled' + | 'false_positives' + | 'from' + | 'interval' + | 'max_signals' + | 'tags' + | 'to' + | 'threat' + | 'throttle' + | 'version' + | 'exceptions_list' + | 'rule_id' +> & { + references: References; + actions: Actions; + enabled: Enabled; + false_positives: FalsePositives; + from: From; + interval: Interval; + max_signals: MaxSignals; + tags: Tags; + to: To; + threat: Threat; + throttle: ThrottleOrNull; + version: Version; + exceptions_list: ListsDefaultArraySchema; + 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 new file mode 100644 index 0000000000000..ebf0b2e591ca9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { getCreateRulesSchemaMock } from './create_rules_schema.mock'; +import { CreateRulesSchema } from './create_rules_schema'; +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(), + timeline_id: '123', + }; + delete schema.timeline_title; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + timeline_id: '123', + timeline_title: '', + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_title" cannot be an empty string']); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + timeline_id: '', + timeline_title: 'some-title', + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_id" cannot be an empty string']); + }); + + test('You cannot have timeline_title without timeline_id', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + timeline_title: 'some-title', + }; + delete schema.timeline_id; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); + }); +}); 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 new file mode 100644 index 0000000000000..aad2a2c4a9206 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts @@ -0,0 +1,105 @@ +/* + * 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 } from './create_rules_schema'; + +export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + 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 (rule.type === 'machine_learning') { + 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 (rule.type === 'machine_learning') { + 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 (rule.type === 'machine_learning') { + if (rule.machine_learning_job_id == null) { + return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateTimelineId = (rule: CreateRulesSchema): string[] => { + if (rule.timeline_id != null) { + if (rule.timeline_title == null) { + return ['when "timeline_id" exists, "timeline_title" must also exist']; + } else if (rule.timeline_id === '') { + return ['"timeline_id" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => { + if (rule.timeline_title != null) { + if (rule.timeline_id == null) { + return ['when "timeline_title" exists, "timeline_id" must also exist']; + } else if (rule.timeline_title === '') { + return ['"timeline_title" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => { + return [ + ...validateAnomalyThreshold(schema), + ...validateQuery(schema), + ...validateLanguage(schema), + ...validateSavedId(schema), + ...validateMachineLearningJobId(schema), + ...validateTimelineId(schema), + ...validateTimelineTitle(schema), + ]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts new file mode 100644 index 0000000000000..3e9799a5ad2f9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { + exportRulesQuerySchema, + exportRulesSchema, + ExportRulesSchema, + ExportRulesQuerySchema, + ExportRulesQuerySchemaDecoded, +} from './export_rules_schema'; +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'; + +describe('create rules schema', () => { + describe('exportRulesSchema', () => { + test('null value or absent values validate', () => { + const payload: Partial = null; + + const decoded = exportRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('empty object does not validate', () => { + const payload = {}; + + const decoded = exportRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + // TODO: Change formatter to display a better value than [object Object] + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "objects"', + 'Invalid value "[object Object]" supplied to ""', + ]); + expect(message.schema).toEqual(payload); + }); + + test('empty object array does validate', () => { + const payload: ExportRulesSchema = { objects: [] }; + + const decoded = exportRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('array with rule_id validates', () => { + const payload: ExportRulesSchema = { objects: [{ rule_id: 'test-1' }] }; + + const decoded = exportRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('array with id does not validate as we do not allow that on purpose since we export rule_id', () => { + const payload: Omit & { objects: [{ id: string }] } = { + objects: [{ id: '4a7ff83d-3055-4bb2-ba68-587b9c6c15a4' }], + }; + + const decoded = exportRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + // TODO: Change formatter to display a better value than [object Object] + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "objects,rule_id"', + 'Invalid value "[object Object]" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('exportRulesQuerySchema', () => { + test('default value for file_name is export.ndjson and default for exclude_export_details is false', () => { + const payload: Partial = {}; + + const decoded = exportRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ExportRulesQuerySchemaDecoded = { + file_name: 'export.ndjson', + exclude_export_details: false, + }; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('file_name validates', () => { + const payload: ExportRulesQuerySchema = { + file_name: 'test.ndjson', + }; + + const decoded = exportRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ExportRulesQuerySchemaDecoded = { + file_name: 'test.ndjson', + exclude_export_details: false, + }; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('file_name does not validate with a number', () => { + const payload: Omit & { file_name: number } = { + file_name: 10, + }; + + const decoded = exportRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "10" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('exclude_export_details validates with a boolean true', () => { + const payload: ExportRulesQuerySchema = { + exclude_export_details: true, + }; + + const decoded = exportRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ExportRulesQuerySchemaDecoded = { + exclude_export_details: true, + file_name: 'export.ndjson', + }; + expect(message.schema).toEqual(expected); + }); + + test('exclude_export_details does not validate with a string', () => { + const payload: Omit & { + exclude_export_details: string; + } = { + exclude_export_details: 'invalid string', + }; + + const decoded = exportRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid string" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts new file mode 100644 index 0000000000000..75fa2da92b787 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts @@ -0,0 +1,33 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { rule_id, FileName, ExcludeExportDetails } from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DefaultExportFileName } from '../types/default_export_file_name'; +import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false'; + +const objects = t.array(t.exact(t.type({ rule_id }))); +export const exportRulesSchema = t.union([t.exact(t.type({ objects })), t.null]); +export type ExportRulesSchema = t.TypeOf; +export type ExportRulesSchemaDecoded = ExportRulesSchema; + +export const exportRulesQuerySchema = t.exact( + t.partial({ file_name: DefaultExportFileName, exclude_export_details: DefaultStringBooleanFalse }) +); + +export type ExportRulesQuerySchema = t.TypeOf; + +export type ExportRulesQuerySchemaDecoded = Omit< + ExportRulesQuerySchema, + 'file_name' | 'exclude_export_details' +> & { + file_name: FileName; + exclude_export_details: ExcludeExportDetails; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.ts new file mode 100644 index 0000000000000..1969d9d798d23 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_statuses_schema.ts @@ -0,0 +1,17 @@ +/* + * 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'; + +export const findRulesStatusesSchema = t.exact( + t.type({ + ids: t.array(t.string), + }) +); + +export type FindRulesStatusesSchema = t.TypeOf; + +export type FindRulesStatusesSchemaDecoded = FindRulesStatusesSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_type_dependents.test.ts new file mode 100644 index 0000000000000..e86e67e47e460 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rule_type_dependents.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { FindRulesSchema } from './find_rules_schema'; +import { findRuleValidateTypeDependents } from './find_rules_type_dependents'; + +describe('find_rules_type_dependents', () => { + test('You can have an empty sort_field and empty sort_order', () => { + const schema: FindRulesSchema = {}; + const errors = findRuleValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You can have both a sort_field and and a sort_order', () => { + const schema: FindRulesSchema = { + sort_field: 'some field', + sort_order: 'asc', + }; + const errors = findRuleValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You cannot have sort_field without sort_order', () => { + const schema: FindRulesSchema = { + sort_field: 'some field', + }; + const errors = findRuleValidateTypeDependents(schema); + expect(errors).toEqual([ + 'when "sort_order" and "sort_field" must exist together or not at all', + ]); + }); + + test('You cannot have sort_order without sort_field', () => { + const schema: FindRulesSchema = { + sort_order: 'asc', + }; + const errors = findRuleValidateTypeDependents(schema); + expect(errors).toEqual([ + 'when "sort_order" and "sort_field" must exist together or not at all', + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts new file mode 100644 index 0000000000000..46e6ee82055a5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { 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 { FindRulesSchema, findRulesSchema } from './find_rules_schema'; + +describe('find_rules_schema', () => { + test('empty objects do validate', () => { + const payload: FindRulesSchema = {}; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + page: 1, + per_page: 20, + }); + }); + + test('all values validate', () => { + const payload: FindRulesSchema = { + per_page: 5, + page: 1, + sort_field: 'some field', + fields: ['field 1', 'field 2'], + filter: 'some filter', + sort_order: 'asc', + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('made up parameters do not validate', () => { + const payload: Partial & { madeUp: string } = { madeUp: 'invalid value' }; + + const decoded = findRulesSchema.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('per_page validates', () => { + const payload: FindRulesSchema = { + per_page: 5, + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).per_page).toEqual(payload.per_page); + }); + + test('page validates', () => { + const payload: FindRulesSchema = { + page: 5, + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).page).toEqual(payload.page); + }); + + test('sort_field validates', () => { + const payload: FindRulesSchema = { + sort_field: 'value', + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).sort_field).toEqual('value'); + }); + + test('fields validates with a string', () => { + const payload: FindRulesSchema = { + fields: ['some value'], + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).fields).toEqual(payload.fields); + }); + + test('fields validates with multiple strings', () => { + const payload: FindRulesSchema = { + fields: ['some value 1', 'some value 2'], + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).fields).toEqual(payload.fields); + }); + + test('fields does not validate with a number', () => { + const payload: Omit & { fields: number } = { + fields: 5, + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "fields"']); + expect(message.schema).toEqual({}); + }); + + test('per_page has a default of 20', () => { + const payload: FindRulesSchema = {}; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).per_page).toEqual(20); + }); + + test('page has a default of 1', () => { + const payload: FindRulesSchema = {}; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).page).toEqual(1); + }); + + test('filter works with a string', () => { + const payload: FindRulesSchema = { + filter: 'some value 1', + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).filter).toEqual(payload.filter); + }); + + test('filter does not work with a number', () => { + const payload: Omit & { filter: number } = { + filter: 5, + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "filter"']); + expect(message.schema).toEqual({}); + }); + + test('sort_order validates with desc and sort_field', () => { + const payload: FindRulesSchema = { + sort_order: 'desc', + sort_field: 'some field', + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as FindRulesSchema).sort_order).toEqual(payload.sort_order); + expect((message.schema as FindRulesSchema).sort_field).toEqual(payload.sort_field); + }); + + test('sort_order does not validate with a string other than asc and desc', () => { + const payload: Omit & { sort_order: string } = { + sort_order: 'some other string', + sort_field: 'some field', + }; + + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some other string" supplied to "sort_order"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts new file mode 100644 index 0000000000000..87076803c9582 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts @@ -0,0 +1,30 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { queryFilter, fields, sort_field, sort_order, PerPage, Page } from '../common/schemas'; +import { DefaultPerPage } from '../types/default_per_page'; +import { DefaultPage } from '../types/default_page'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const findRulesSchema = t.exact( + t.partial({ + fields, + filter: queryFilter, + per_page: DefaultPerPage, // defaults to "20" if not sent in during decode + page: DefaultPage, // defaults to "1" if not sent in during decode + sort_field, + sort_order, + }) +); + +export type FindRulesSchema = t.TypeOf; +export type FindRulesSchemaDecoded = Omit & { + per_page: PerPage; + page: Page; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_type_dependents.ts new file mode 100644 index 0000000000000..6916f102e4f57 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_type_dependents.ts @@ -0,0 +1,23 @@ +/* + * 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 { FindRulesSchema } from './find_rules_schema'; + +export const validateSortOrder = (find: FindRulesSchema): string[] => { + if (find.sort_order != null || find.sort_field != null) { + if (find.sort_order == null || find.sort_field == null) { + return ['when "sort_order" and "sort_field" must exist together or not at all']; + } else { + return []; + } + } else { + return []; + } +}; + +export const findRuleValidateTypeDependents = (schema: FindRulesSchema): string[] => { + return [...validateSortOrder(schema)]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts new file mode 100644 index 0000000000000..92fab202c9ddc --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts @@ -0,0 +1,44 @@ +/* + * 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 { ImportRulesSchema, ImportRulesSchemaDecoded } from './import_rules_schema'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +export const getImportRulesSchemaMock = (): ImportRulesSchema => ({ + 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 getImportRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ({ + 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', + 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', + immutable: false, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts new file mode 100644 index 0000000000000..be2c3e046fe91 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -0,0 +1,1579 @@ +/* + * 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 { 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 { + ImportRulesSchema, + importRulesSchema, + ImportRulesSchemaDecoded, + importRulesQuerySchema, + ImportRulesQuerySchema, + importRulesPayloadSchema, + ImportRulesPayloadSchema, +} from './import_rules_schema'; +import { + getImportRulesSchemaMock, + getImportRulesSchemaDecodedMock, +} from './import_rules_schema.mock'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +describe('import rules schema', () => { + test('empty objects do not validate', () => { + const payload: Partial = {}; + + const decoded = importRulesSchema.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"', + 'Invalid value "undefined" supplied to "rule_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('made up values do not validate', () => { + const payload: ImportRulesSchema & { madeUp: string } = { + ...getImportRulesSchemaMock(), + madeUp: 'hi', + }; + + const decoded = importRulesSchema.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 = importRulesSchema.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 = importRulesSchema.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 = importRulesSchema.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 = importRulesSchema.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, name] does not validate', () => { + const payload: Partial = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + + const decoded = importRulesSchema.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 = importRulesSchema.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 = importRulesSchema.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 = importRulesSchema.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 = importRulesSchema.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: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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: [], + immutable: false, + }; + 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 = importRulesSchema.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: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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: [], + immutable: false, + }; + 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: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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: [], + immutable: false, + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { + const payload: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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: [], + immutable: false, + }; + 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: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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: [], + immutable: false, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can send in an empty array to threat', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + threat: [], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + 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: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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', + immutable: false, + 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); + }); + + test('allows references to be sent as valid', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + references: ['index-1'], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + references: ['index-1'], + }; + expect(message.schema).toEqual(expected); + }); + + test('defaults references to an array if it is not sent in', () => { + const { references, ...noReferences } = getImportRulesSchemaMock(); + const decoded = importRulesSchema.decode(noReferences); + const checked = exactCheck(noReferences, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + references: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('references cannot be numbers', () => { + const payload: Omit & { references: number[] } = { + ...getImportRulesSchemaMock(), + references: [5], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('indexes cannot be numbers', () => { + const payload: Omit & { index: number[] } = { + ...getImportRulesSchemaMock(), + index: [5], + }; + + const decoded = importRulesSchema.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 } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noInterval, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { interval: expectedInterval, ...expectedNoInterval } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...expectedNoInterval, + interval: '5m', + }; + expect(message.schema).toEqual(expected); + }); + + test('defaults max signals to 100', () => { + const { max_signals, ...noMaxSignals } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noMaxSignals, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { + max_signals: expectedMaxSignals, + ...expectedNoMaxSignals + } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...expectedNoMaxSignals, + max_signals: 100, + }; + expect(message.schema).toEqual(expected); + }); + + test('saved_query type can have filters with it', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + filters: [], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('filters cannot be a string', () => { + const payload: Omit & { filters: string } = { + ...getImportRulesSchemaMock(), + filters: 'some string', + }; + + const decoded = importRulesSchema.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: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + language: 'kuery', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + language: 'kuery', + }; + expect(message.schema).toEqual(expected); + }); + + test('language validates with lucene', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + language: 'lucene', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + language: 'lucene', + }; + expect(message.schema).toEqual(expected); + }); + + test('language does not validate with something made up', () => { + const payload: Omit & { language: string } = { + ...getImportRulesSchemaMock(), + language: 'something-made-up', + }; + + const decoded = importRulesSchema.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: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + max_signals: -1, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('max_signals cannot be zero', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + max_signals: 0, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('max_signals can be 1', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + max_signals: 1, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + max_signals: 1, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can optionally send in an array of tags', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + tags: ['tag_1', 'tag_2'], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + 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[] } = { + ...getImportRulesSchemaMock(), + tags: [0, 1, 2], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "2" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "framework"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getImportRulesSchemaMock(), + threat: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "framework"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "tactic"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getImportRulesSchemaMock(), + threat: [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "tactic"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "technique"', () => { + const payload: Omit & { + threat: Array>>; + } = { + ...getImportRulesSchemaMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "technique"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You can optionally send in an array of false positives', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + false_positives: ['false_1', 'false_2'], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + 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[] } = { + ...getImportRulesSchemaMock(), + false_positives: [5, 4], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to ""', + 'Invalid value "4" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the immutable to a number when trying to create a rule', () => { + const payload: Omit & { immutable: number } = { + ...getImportRulesSchemaMock(), + immutable: 5, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "immutable"']); + expect(message.schema).toEqual({}); + }); + + test('You can optionally set the immutable to be false', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + immutable: false, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getImportRulesSchemaDecodedMock()); + }); + + test('You cannot set the immutable to be true', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + immutable: true, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "true" supplied to "immutable"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the immutable to be a number', () => { + const payload: Omit & { immutable: number } = { + ...getImportRulesSchemaMock(), + immutable: 5, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "immutable"']); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the risk_score to 101', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + risk_score: 101, + }; + + const decoded = importRulesSchema.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: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + risk_score: -1, + }; + + const decoded = importRulesSchema.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: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + risk_score: 0, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + risk_score: 0, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can set the risk_score to 100', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + risk_score: 100, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + risk_score: 100, + }; + expect(message.schema).toEqual(expected); + }); + + test('You can set meta to any object you want', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot create meta as a string', () => { + const payload: Omit & { meta: string } = { + ...getImportRulesSchemaMock(), + meta: 'should not work', + }; + + const decoded = importRulesSchema.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 } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noQuery, + filters: [], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { query: expectedQuery, ...expectedNoQuery } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...expectedNoQuery, + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('validates with timeline_id and timeline_title', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id is required and you cannot get by with just id', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', + }; + delete payload.rule_id; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "rule_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it validates with created_at, updated_at, created_by, updated_by values', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + created_at: '2020-01-09T06:15:24.749Z', + updated_at: '2020-01-09T06:15:24.749Z', + created_by: 'Braden Hassanabad', + updated_by: 'Evan Hassanabad', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + created_at: '2020-01-09T06:15:24.749Z', + updated_at: '2020-01-09T06:15:24.749Z', + created_by: 'Braden Hassanabad', + updated_by: 'Evan Hassanabad', + }; + expect(message.schema).toEqual(expected); + }); + + test('it does not validate with epoch strings for created_at', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + created_at: '1578550728650', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1578550728650" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it does not validate with epoch strings for updated_at', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + updated_at: '1578550728650', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1578550728650" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + describe('importRulesQuerySchema', () => { + test('overwrite gets a default value of false', () => { + const payload: ImportRulesQuerySchema = {}; + + const decoded = importRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + overwrite: false, + }); + }); + + test('overwrite validates with a boolean true', () => { + const payload: ImportRulesQuerySchema = { overwrite: true }; + + const decoded = importRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + overwrite: true, + }); + }); + + test('overwrite does not validate with a weird string', () => { + const payload: Omit & { overwrite: string } = { + overwrite: 'invalid-string', + }; + + const decoded = importRulesQuerySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid-string" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('importRulesPayloadSchema', () => { + test('does not validate with an empty object', () => { + const payload = {}; + + const decoded = importRulesPayloadSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "file"', + ]); + expect(message.schema).toEqual({}); + }); + + test('does not validate with a made string', () => { + const payload: Omit & { madeUpKey: string } = { + madeUpKey: 'madeupstring', + }; + + const decoded = importRulesPayloadSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "file"', + ]); + expect(message.schema).toEqual({}); + }); + + test('does validate with a file object', () => { + const payload: ImportRulesPayloadSchema = { file: {} }; + + const decoded = importRulesPayloadSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + test('The default for "from" will be "now-6m"', () => { + const { from, ...noFrom } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noFrom, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { from: expectedFrom, ...expectedNoFrom } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...expectedNoFrom, + from: 'now-6m', + }; + expect(message.schema).toEqual(expected); + }); + + test('The default for "to" will be "now"', () => { + const { to, ...noTo } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noTo, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { to: expectedTo, ...expectedNoTo } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...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 } = { + ...getImportRulesSchemaMock(), + severity: 'junk', + }; + + const decoded = importRulesSchema.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 } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noActions, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { actions: expectedActions, ...expectedNoActions } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...expectedNoActions, + actions: [], + }; + expect(message.schema).toEqual(expected); + }); + test('You cannot send in an array of actions that are missing "group"', () => { + const payload: Omit = { + ...getImportRulesSchemaMock(), + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "group"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + const payload: Omit = { + ...getImportRulesSchemaMock(), + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + const payload: Omit = { + ...getImportRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', params: {} }], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + const payload: Omit = { + ...getImportRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "params"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are including "actionTypeId"', () => { + const payload: Omit = { + ...getImportRulesSchemaMock(), + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('The default for "throttle" will be null', () => { + const { throttle, ...noThrottle } = getImportRulesSchemaMock(); + const payload: ImportRulesSchema = { + ...noThrottle, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const { throttle: expectedThrottle, ...expectedNoThrottle } = getImportRulesSchemaDecodedMock(); + const expected: ImportRulesSchemaDecoded = { + ...expectedNoThrottle, + throttle: null, + }; + expect(message.schema).toEqual(expected); + }); + + describe('note', () => { + test('You can set note to a string', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + note: '# documentation markdown here', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + note: '# documentation markdown here', + }; + expect(message.schema).toEqual(expected); + }); + + test('You can set note to an empty string', () => { + const payload: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + note: '', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + ...getImportRulesSchemaDecodedMock(), + note: '', + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot create note as an object', () => { + const payload: Omit & { note: {} } = { + ...getImportRulesSchemaMock(), + note: { + somethingHere: 'something else', + }, + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + // TODO: Fix/Change the formatErrors to be better able to handle objects + 'Invalid value "[object Object]" 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: ImportRulesSchema = { + 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 = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + 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: [], + immutable: false, + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + }); + + // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin + describe.skip('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + + test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts new file mode 100644 index 0000000000000..a2110263e8e51 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -0,0 +1,180 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + description, + anomaly_threshold, + 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, + ThrottleOrNull, + note, + Version, + References, + Actions, + Enabled, + FalsePositives, + From, + Interval, + language, + query, + rule_id, + id, + created_at, + updated_at, + created_by, + updated_by, +} from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DefaultStringArray } from '../types/default_string_array'; +import { DefaultActionsArray } from '../types/default_actions_array'; +import { DefaultBooleanTrue } from '../types/default_boolean_true'; +import { DefaultFromString } from '../types/default_from_string'; +import { DefaultIntervalString } from '../types/default_interval_string'; +import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; +import { DefaultToString } from '../types/default_to_string'; +import { DefaultThreatArray } from '../types/default_threat_array'; +import { DefaultThrottleNull } from '../types/default_throttle_null'; +import { DefaultVersionNumber } from '../types/default_version_number'; +import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; +import { OnlyFalseAllowed } from '../types/only_false_allowed'; +import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false'; + +/** + * Differences from this and the createRulesSchema are + * - rule_id is required + * - id is optional (but ignored in the import code - rule_id is exclusively used for imports) + * - immutable is optional but if it is any value other than false it will be rejected + * - created_at is optional (but ignored in the import code) + * - updated_at is optional (but ignored in the import code) + * - created_by is optional (but ignored in the import code) + * - updated_by is optional (but ignored in the import code) + */ +export const importRulesSchema = t.intersection([ + t.exact( + t.type({ + description, + risk_score, + name, + severity, + type, + rule_id, + }) + ), + 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 + enabled: DefaultBooleanTrue, // defaults to true 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 + index, // defaults to undefined if not set during decode + immutable: OnlyFalseAllowed, // defaults to "false" 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 + // 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 + 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 + throttle: DefaultThrottleNull, // defaults to "null" 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: ListsDefaultArray, // defaults to empty array if not set during decode + created_at, // defaults "undefined" if not set during decode + updated_at, // defaults "undefined" if not set during decode + created_by, // defaults "undefined" if not set during decode + updated_by, // defaults "undefined" if not set during decode + }) + ), +]); + +export type ImportRulesSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ImportRulesSchemaDecoded = Omit< + ImportRulesSchema, + | 'references' + | 'actions' + | 'enabled' + | 'false_positives' + | 'from' + | 'interval' + | 'max_signals' + | 'tags' + | 'to' + | 'threat' + | 'throttle' + | 'version' + | 'exceptions_list' + | 'rule_id' + | 'immutable' +> & { + references: References; + actions: Actions; + enabled: Enabled; + false_positives: FalsePositives; + from: From; + interval: Interval; + max_signals: MaxSignals; + tags: Tags; + to: To; + threat: Threat; + throttle: ThrottleOrNull; + version: Version; + exceptions_list: ListsDefaultArraySchema; + rule_id: RuleId; + immutable: false; +}; + +export const importRulesQuerySchema = t.exact( + t.partial({ + overwrite: DefaultStringBooleanFalse, + }) +); + +export type ImportRulesQuerySchema = t.TypeOf; +export type ImportRulesQuerySchemaDecoded = Omit & { + overwrite: boolean; +}; + +export const importRulesPayloadSchema = t.exact( + t.type({ + file: t.object, + }) +); + +export type ImportRulesPayloadSchema = t.TypeOf; + +export type ImportRulesPayloadSchemaDecoded = ImportRulesPayloadSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts new file mode 100644 index 0000000000000..f9b989c81e533 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { getImportRulesSchemaMock } from './import_rules_schema.mock'; +import { ImportRulesSchema } from './import_rules_schema'; +import { importRuleValidateTypeDependents } from './import_rules_type_dependents'; + +describe('import_rules_type_dependents', () => { + test('saved_id is required when type is saved_query and will not validate without out', () => { + const schema: ImportRulesSchema = { ...getImportRulesSchemaMock(), type: 'saved_query' }; + delete schema.saved_id; + const errors = importRuleValidateTypeDependents(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: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + type: 'saved_query', + saved_id: '123', + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + timeline_id: '123', + }; + delete schema.timeline_title; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + timeline_id: '123', + timeline_title: '', + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_title" cannot be an empty string']); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + timeline_id: '', + timeline_title: 'some-title', + }; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_id" cannot be an empty string']); + }); + + test('You cannot have timeline_title without timeline_id', () => { + const schema: ImportRulesSchema = { + ...getImportRulesSchemaMock(), + timeline_title: 'some-title', + }; + delete schema.timeline_id; + const errors = importRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts new file mode 100644 index 0000000000000..59191a4fe3121 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_type_dependents.ts @@ -0,0 +1,105 @@ +/* + * 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 { ImportRulesSchema } from './import_rules_schema'; + +export const validateAnomalyThreshold = (rule: ImportRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.anomaly_threshold == null) { + return ['when "type" is "machine_learning" anomaly_threshold is required']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateQuery = (rule: ImportRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.query != null) { + return ['when "type" is "machine_learning", "query" cannot be set']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateLanguage = (rule: ImportRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.language != null) { + return ['when "type" is "machine_learning", "language" cannot be set']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateSavedId = (rule: ImportRulesSchema): 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: ImportRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.machine_learning_job_id == null) { + return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateTimelineId = (rule: ImportRulesSchema): string[] => { + if (rule.timeline_id != null) { + if (rule.timeline_title == null) { + return ['when "timeline_id" exists, "timeline_title" must also exist']; + } else if (rule.timeline_id === '') { + return ['"timeline_id" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateTimelineTitle = (rule: ImportRulesSchema): string[] => { + if (rule.timeline_title != null) { + if (rule.timeline_id == null) { + return ['when "timeline_title" exists, "timeline_id" must also exist']; + } else if (rule.timeline_title === '') { + return ['"timeline_title" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const importRuleValidateTypeDependents = (schema: ImportRulesSchema): string[] => { + return [ + ...validateAnomalyThreshold(schema), + ...validateQuery(schema), + ...validateLanguage(schema), + ...validateSavedId(schema), + ...validateMachineLearningJobId(schema), + ...validateTimelineId(schema), + ...validateTimelineTitle(schema), + ]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts new file mode 100644 index 0000000000000..a388e69332072 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rule_type_dependents.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { getPatchRulesSchemaMock } from './patch_rules_schema.mock'; +import { PatchRulesSchema } from './patch_rules_schema'; +import { patchRuleValidateTypeDependents } from './patch_rules_type_dependents'; + +describe('patch_rules_type_dependents', () => { + test('saved_id is required when type is saved_query and validates with it', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + type: 'saved_query', + saved_id: '123', + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + timeline_id: '123', + }; + delete schema.timeline_title; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + timeline_id: '123', + timeline_title: '', + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_title" cannot be an empty string']); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + timeline_id: '', + timeline_title: 'some-title', + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_id" cannot be an empty string']); + }); + + test('You cannot have timeline_title without timeline_id', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + timeline_title: 'some-title', + }; + delete schema.timeline_id; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); + }); + + test('You cannot have both an id and a rule_id', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + id: 'some-id', + rule_id: 'some-rule-id', + }; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['both "id" and "rule_id" cannot exist, choose one or the other']); + }); + + test('You must set either an id or a rule_id', () => { + const schema: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + }; + delete schema.id; + delete schema.rule_id; + const errors = patchRuleValidateTypeDependents(schema); + expect(errors).toEqual(['either "id" or "rule_id" must be set']); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts new file mode 100644 index 0000000000000..7b86c02e5c475 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { patchRulesBulkSchema, PatchRulesBulkSchema } from './patch_rules_bulk_schema'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight } from '../../../test_utils'; +import { formatErrors } from '../../../format_errors'; +import { PatchRulesSchema } from './patch_rules_schema'; + +// only the basics of testing are here. +// see: patch_rules_schema.test.ts for the bulk of the validation tests +// this just wraps patchRulesSchema in an array +describe('patch_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + const payload: PatchRulesBulkSchema = []; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(output.errors).toEqual([]); + expect(output.schema).toEqual([]); + }); + + test('made up values do not validate for a single element', () => { + const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }]; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUp"']); + expect(output.schema).toEqual({}); + }); + + test('single array of [id] does validate', () => { + const payload: PatchRulesBulkSchema = [{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' }]; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('two arrays of [id] validate', () => { + const payload: PatchRulesBulkSchema = [ + { id: '4125761e-51da-4de9-a0c8-42824f532ddb' }, + { id: '192f403d-b285-4251-9e8b-785fcfcf22e8' }, + ]; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('can set "note" to be a string', () => { + const payload: PatchRulesBulkSchema = [ + { id: '4125761e-51da-4de9-a0c8-42824f532ddb' }, + { note: 'hi' }, + ]; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('can set "note" to be an empty string', () => { + const payload: PatchRulesBulkSchema = [ + { id: '4125761e-51da-4de9-a0c8-42824f532ddb' }, + { note: '' }, + ]; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('cannot set "note" to be anything other than a string', () => { + const payload: Array & { note?: object }> = [ + { id: '4125761e-51da-4de9-a0c8-42824f532ddb' }, + { note: { someprop: 'some value here' } }, + ]; + + const decoded = patchRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + // TODO: Fix the formatter to give something better than [object Object] + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "[object Object]" supplied to "note"', + ]); + expect(output.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts new file mode 100644 index 0000000000000..272e36751e37b --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { patchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema'; + +export const patchRulesBulkSchema = t.array(patchRulesSchema); +export type PatchRulesBulkSchema = t.TypeOf; + +export type PatchRulesBulkSchemaDecoded = PatchRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.mock.ts new file mode 100644 index 0000000000000..7b4f632cdebf0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PatchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema'; + +export const getPatchRulesSchemaMock = (): PatchRulesSchema => ({ + 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 getPatchRulesSchemaDecodedMock = (): PatchRulesSchemaDecoded => + getPatchRulesSchemaMock(); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts new file mode 100644 index 0000000000000..921e07a29609c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts @@ -0,0 +1,1153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { patchRulesSchema, PatchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema'; +import { getPatchRulesSchemaMock, getPatchRulesSchemaDecodedMock } from './patch_rules_schema.mock'; +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'; + +describe('patch_rules_schema', () => { + test('made up values do not validate', () => { + const payload: PatchRulesSchema & { madeUp: string } = { + ...getPatchRulesSchemaMock(), + madeUp: 'hi', + }; + + const decoded = patchRulesSchema.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('[id] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, risk_score] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + risk_score: 10, + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + risk_score: 10, + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, name] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to, name] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, name, severity] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to, name, severity] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, name, severity, type] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to, name, severity, type] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to, name, severity, type, interval] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + const payload: PatchRulesSchema = { + 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', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + 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', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + expect(message.schema).toEqual(expected); + }); + + test('[id, description, from, to, index, name, severity, type, filters] does validate', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, type, filters] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('allows references to be sent as a valid value to patch with', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }; + expect(message.schema).toEqual(expected); + }); + + test('does not default references to an array', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as PatchRulesSchemaDecoded).references).toEqual(undefined); + }); + + test('does not default interval', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as PatchRulesSchemaDecoded).interval).toEqual(undefined); + }); + + test('does not default max_signals', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as PatchRulesSchemaDecoded).max_signals).toEqual(undefined); + }); + + test('references cannot be numbers', () => { + const payload: Omit & { references: number[] } = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + references: [5], + }; + + const decoded = patchRulesSchema.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[] } = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + index: [5], + }; + + const decoded = patchRulesSchema.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('saved_id is not required when type is saved_query and will validate without it', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + type: 'saved_query', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + type: 'saved_query', + }; + expect(message.schema).toEqual(expected); + }); + + test('saved_id validates with type:saved_query', () => { + const payload: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + type: 'saved_query', + saved_id: 'some id', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + type: 'saved_query', + saved_id: 'some id', + }; + expect(message.schema).toEqual(expected); + }); + + test('saved_query type can have filters with it', () => { + const payload: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + saved_id: 'some id', + filters: [], + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + saved_id: 'some id', + filters: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('language validates with kuery', () => { + const payload: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + query: 'some query', + language: 'kuery', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + query: 'some query', + language: 'kuery', + }; + expect(message.schema).toEqual(expected); + }); + + test('language validates with lucene', () => { + const payload: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + query: 'some query', + language: 'lucene', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + query: 'some query', + language: 'lucene', + }; + expect(message.schema).toEqual(expected); + }); + + test('language does not validate with something made up', () => { + const payload: Omit & { language: string } = { + ...getPatchRulesSchemaMock(), + query: 'some query', + language: 'something-made-up', + }; + + const decoded = patchRulesSchema.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: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + query: 'some query', + max_signals: -1, + }; + + const decoded = patchRulesSchema.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: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + query: 'some query', + max_signals: 0, + }; + + const decoded = patchRulesSchema.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: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + query: 'some query', + max_signals: 1, + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + query: 'some query', + max_signals: 1, + }; + expect(message.schema).toEqual(expected); + }); + + test('meta can be patched', () => { + const payload: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + meta: { whateverYouWant: 'anything_at_all' }, + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + meta: { whateverYouWant: 'anything_at_all' }, + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot patch meta as a string', () => { + const payload: Omit & { meta: string } = { + ...getPatchRulesSchemaMock(), + meta: 'should not work', + }; + + const decoded = patchRulesSchema.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('filters cannot be a string', () => { + const payload: Omit & { filters: string } = { + ...getPatchRulesSchemaMock(), + filters: 'should not work', + }; + + const decoded = patchRulesSchema.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 "filters"', + ]); + expect(message.schema).toEqual({}); + }); + + test('threat is not defaulted to empty array on patch', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as PatchRulesSchemaDecoded).threat).toEqual(undefined); + }); + + test('threat is not defaulted to undefined on patch with empty array', () => { + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + threat: [], + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as PatchRulesSchemaDecoded).threat).toEqual([]); + }); + + test('threat is valid when updated with all sub-objects', () => { + const threat: PatchRulesSchema['threat'] = [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ]; + const payload: PatchRulesSchema = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + threat, + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('threat is invalid when updated with missing property framework', () => { + const threat: Omit = [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ]; + const payload: Omit = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + threat, + }; + + const decoded = patchRulesSchema.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('threat is invalid when updated with missing tactic sub-object', () => { + const threat: Omit = [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ]; + + const payload: Omit = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + threat, + }; + + const decoded = patchRulesSchema.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('threat is invalid when updated with missing technique', () => { + const threat: Omit = [ + { + framework: 'fake', + tactic: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ]; + + const payload: Omit = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + threat, + }; + + const decoded = patchRulesSchema.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('validates with timeline_id and timeline_title', () => { + const payload: PatchRulesSchema = { + ...getPatchRulesSchemaMock(), + timeline_id: 'some-id', + timeline_title: 'some-title', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + ...getPatchRulesSchemaDecodedMock(), + timeline_id: 'some-id', + timeline_title: 'some-title', + }; + 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 } = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + severity: 'junk', + }; + + const decoded = patchRulesSchema.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({}); + }); + + describe('note', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, note] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + note: '# some documentation markdown', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + note: '# some documentation markdown', + }; + expect(message.schema).toEqual(expected); + }); + + test('note can be patched', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + note: '# new documentation markdown', + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + note: '# new documentation markdown', + }; + expect(message.schema).toEqual(expected); + }); + + test('You cannot patch note as an object', () => { + const payload: Omit & { note: object } = { + id: 'b8f95e17-681f-407f-8a5e-b832a77d3831', + note: { + someProperty: 'something else here', + }, + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + // TODO: Change the formatter to output something more readable than [object Object] + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[object Object]" supplied to "note"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + const payload: Omit = { + ...getPatchRulesSchemaMock(), + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = patchRulesSchema.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 = { + ...getPatchRulesSchemaMock(), + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = patchRulesSchema.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 "params"', () => { + const payload: Omit = { + ...getPatchRulesSchemaMock(), + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + }; + + const decoded = patchRulesSchema.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 = { + ...getPatchRulesSchemaMock(), + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + + const decoded = patchRulesSchema.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({}); + }); + + // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin + describe.skip('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + + test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts new file mode 100644 index 0000000000000..605e0272bbb4c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -0,0 +1,90 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + description, + anomaly_threshold, + filters, + index, + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + machine_learning_job_id, + risk_score, + rule_id, + name, + severity, + type, + note, + version, + actions, + false_positives, + interval, + max_signals, + from, + enabled, + tags, + threat, + throttle, + references, + to, + language, + listAndOrUndefined, + query, + id, +} from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +/** + * All of the patch elements should default to undefined if not set + */ +export const patchRulesSchema = t.exact( + t.partial({ + description, + risk_score, + name, + severity, + type, + id, + actions, + anomaly_threshold, + enabled, + false_positives, + filters, + from, + rule_id, + index, + interval, + query, + language, + // TODO: output_index: This should be removed eventually + output_index, + saved_id, + timeline_id, + timeline_title, + meta, + machine_learning_job_id, + max_signals, + tags, + to, + threat, + throttle, + references, + note, + version, + exceptions_list: listAndOrUndefined, + }) +); + +export type PatchRulesSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type PatchRulesSchemaDecoded = PatchRulesSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts new file mode 100644 index 0000000000000..554cdb822762f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_type_dependents.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PatchRulesSchema } from './patch_rules_schema'; + +export const validateQuery = (rule: PatchRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.query != null) { + return ['when "type" is "machine_learning", "query" cannot be set']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateLanguage = (rule: PatchRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + if (rule.language != null) { + return ['when "type" is "machine_learning", "language" cannot be set']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateTimelineId = (rule: PatchRulesSchema): string[] => { + if (rule.timeline_id != null) { + if (rule.timeline_title == null) { + return ['when "timeline_id" exists, "timeline_title" must also exist']; + } else if (rule.timeline_id === '') { + return ['"timeline_id" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateTimelineTitle = (rule: PatchRulesSchema): string[] => { + if (rule.timeline_title != null) { + if (rule.timeline_id == null) { + return ['when "timeline_title" exists, "timeline_id" must also exist']; + } else if (rule.timeline_title === '') { + return ['"timeline_title" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateId = (rule: PatchRulesSchema): string[] => { + if (rule.id != null && rule.rule_id != null) { + return ['both "id" and "rule_id" cannot exist, choose one or the other']; + } else if (rule.id == null && rule.rule_id == null) { + return ['either "id" or "rule_id" must be set']; + } else { + return []; + } +}; + +export const patchRuleValidateTypeDependents = (schema: PatchRulesSchema): string[] => { + return [ + ...validateId(schema), + ...validateQuery(schema), + ...validateLanguage(schema), + ...validateTimelineId(schema), + ...validateTimelineTitle(schema), + ]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts new file mode 100644 index 0000000000000..4728f7dd84cbd --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { queryRulesBulkSchema, QueryRulesBulkSchema } from './query_rules_bulk_schema'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight } from '../../../test_utils'; +import { formatErrors } from '../../../format_errors'; + +// only the basics of testing are here. +// see: query_rules_schema.test.ts for the bulk of the validation tests +// this just wraps queryRulesSchema in an array +describe('query_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + const payload: QueryRulesBulkSchema = []; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([]); + }); + + test('non uuid being supplied to id does not validate', () => { + const payload: QueryRulesBulkSchema = [ + { + id: '1', + }, + ]; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['Invalid value "1" supplied to "id"']); + expect(output.schema).toEqual({}); + }); + + test('both rule_id and id being supplied do validate', () => { + const payload: QueryRulesBulkSchema = [ + { + rule_id: '1', + id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f', + }, + ]; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('only id validates with two elements', () => { + const payload: QueryRulesBulkSchema = [ + { id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' }, + { id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' }, + ]; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('only rule_id validates', () => { + const payload: QueryRulesBulkSchema = [{ rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' }]; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('only rule_id validates with two elements', () => { + const payload: QueryRulesBulkSchema = [ + { rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' }, + { rule_id: '2' }, + ]; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); + + test('both id and rule_id validates with two separate elements', () => { + const payload: QueryRulesBulkSchema = [ + { id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' }, + { rule_id: '2' }, + ]; + + const decoded = queryRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual(payload); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.ts new file mode 100644 index 0000000000000..a5611bdc2f12f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { queryRulesSchema, QueryRulesSchemaDecoded } from './query_rules_schema'; + +export const queryRulesBulkSchema = t.array(queryRulesSchema); +export type QueryRulesBulkSchema = t.TypeOf; + +export type QueryRulesBulkSchemaDecoded = QueryRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts new file mode 100644 index 0000000000000..c9cb6f310961a --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { queryRulesSchema, QueryRulesSchema } from './query_rules_schema'; +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'; + +describe('query_rules_schema', () => { + test('empty objects do validate', () => { + const payload: Partial = {}; + + const decoded = queryRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.ts similarity index 54% rename from x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_schema.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.ts index 86a731699d1ea..cb8f21128b052 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.ts @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; +import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { rule_id, id } from './schemas'; +import { rule_id, id } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -export const queryRulesSchema = Joi.object({ - rule_id, - id, -}).xor('id', 'rule_id'); +export const queryRulesSchema = t.exact( + t.partial({ + rule_id, + id, + }) +); + +export type QueryRulesSchema = t.TypeOf; +export type QueryRulesSchemaDecoded = QueryRulesSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_type_dependents.test.ts new file mode 100644 index 0000000000000..ad7f368d4eede --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_type_dependents.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { QueryRulesSchema } from './query_rules_schema'; +import { queryRuleValidateTypeDependents } from './query_rules_type_dependents'; + +describe('query_rules_type_dependents', () => { + test('You cannot have both an id and a rule_id', () => { + const schema: QueryRulesSchema = { + id: 'some-id', + rule_id: 'some-rule-id', + }; + const errors = queryRuleValidateTypeDependents(schema); + expect(errors).toEqual(['both "id" and "rule_id" cannot exist, choose one or the other']); + }); + + test('You must set either an id or a rule_id', () => { + const schema: QueryRulesSchema = {}; + const errors = queryRuleValidateTypeDependents(schema); + expect(errors).toEqual(['either "id" or "rule_id" must be set']); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_type_dependents.ts new file mode 100644 index 0000000000000..f3e602dff0f06 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_type_dependents.ts @@ -0,0 +1,21 @@ +/* + * 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 { QueryRulesSchema } from './query_rules_schema'; + +export const validateId = (rule: QueryRulesSchema): string[] => { + if (rule.id != null && rule.rule_id != null) { + return ['both "id" and "rule_id" cannot exist, choose one or the other']; + } else if (rule.id == null && rule.rule_id == null) { + return ['either "id" or "rule_id" must be set']; + } else { + return []; + } +}; + +export const queryRuleValidateTypeDependents = (schema: QueryRulesSchema): string[] => { + return [...validateId(schema)]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.test.ts new file mode 100644 index 0000000000000..23e5fa7e721b0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.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. + */ +/* + * 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 { QuerySignalsSchema, querySignalsSchema } from './query_signals_index_schema'; +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'; + +describe('query, aggs, size, _source and track_total_hits on signals index', () => { + test('query, aggs, size, _source and track_total_hits simultaneously', () => { + const payload: QuerySignalsSchema = { + query: {}, + aggs: {}, + size: 1, + track_total_hits: true, + _source: ['field'], + }; + + const decoded = querySignalsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('query, only', () => { + const payload: QuerySignalsSchema = { + query: {}, + }; + + const decoded = querySignalsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('aggs only', () => { + const payload: QuerySignalsSchema = { + aggs: {}, + }; + + const decoded = querySignalsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('size only', () => { + const payload: QuerySignalsSchema = { + size: 1, + }; + + const decoded = querySignalsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('track_total_hits only', () => { + const payload: QuerySignalsSchema = { + track_total_hits: true, + }; + + const decoded = querySignalsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('_source only', () => { + const payload: QuerySignalsSchema = { + _source: ['field'], + }; + + const decoded = querySignalsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts new file mode 100644 index 0000000000000..3a050c9208f77 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts @@ -0,0 +1,21 @@ +/* + * 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 { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; + +export const querySignalsSchema = t.exact( + t.partial({ + query: t.object, + aggs: t.object, + size: PositiveIntegerGreaterThanZero, + track_total_hits: t.boolean, + _source: t.array(t.string), + }) +); + +export type QuerySignalsSchema = t.TypeOf; +export type QuerySignalsSchemaDecoded = QuerySignalsSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts new file mode 100644 index 0000000000000..dc889892ba83e --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { setSignalsStatusSchema, SetSignalsStatusSchema } from './set_signal_status_schema'; +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'; + +describe('set signal status schema', () => { + test('signal_ids and status is valid', () => { + const payload: SetSignalsStatusSchema = { + signal_ids: ['somefakeid'], + status: 'open', + }; + + const decoded = setSignalsStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('query and status is valid', () => { + const payload: SetSignalsStatusSchema = { + query: {}, + status: 'open', + }; + + const decoded = setSignalsStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('signal_ids and missing status is invalid', () => { + const payload: Omit = { + signal_ids: ['somefakeid'], + }; + + const decoded = setSignalsStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "status"', + ]); + expect(message.schema).toEqual({}); + }); + + test('query and missing status is invalid', () => { + const payload: Omit = { + query: {}, + }; + + const decoded = setSignalsStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "status"', + ]); + expect(message.schema).toEqual({}); + }); + + test('signal_ids is present but status has wrong value', () => { + const payload: Omit & { status: 'fakeVal' } = { + query: {}, + status: 'fakeVal', + }; + + const decoded = setSignalsStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "fakeVal" supplied to "status"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts new file mode 100644 index 0000000000000..0e922aeaf8cdf --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts @@ -0,0 +1,24 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { signal_ids, signal_status_query, status } from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const setSignalsStatusSchema = t.intersection([ + t.type({ + status, + }), + t.partial({ + signal_ids, + query: signal_status_query, + }), +]); + +export type SetSignalsStatusSchema = t.TypeOf; +export type SetSignalsStatusSchemaDecoded = SetSignalsStatusSchema; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_type_dependents.test.ts new file mode 100644 index 0000000000000..b79af2fef5c97 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_type_dependents.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { setSignalStatusValidateTypeDependents } from './set_signal_status_type_dependents'; +import { SetSignalsStatusSchema } from './set_signal_status_schema'; + +describe('update_rules_type_dependents', () => { + test('You can have just a "signals_id"', () => { + const schema: SetSignalsStatusSchema = { + status: 'open', + signal_ids: ['some-id'], + }; + const errors = setSignalStatusValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You can have just a "query"', () => { + const schema: SetSignalsStatusSchema = { + status: 'open', + query: {}, + }; + const errors = setSignalStatusValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); + + test('You cannot have both a "signals_id" and a "query"', () => { + const schema: SetSignalsStatusSchema = { + status: 'open', + query: {}, + signal_ids: ['some-id'], + }; + const errors = setSignalStatusValidateTypeDependents(schema); + expect(errors).toEqual(['both "signal_ids" and "query" cannot exist, choose one or the other']); + }); + + test('You must set either an "signals_id" and a "query"', () => { + const schema: SetSignalsStatusSchema = { + status: 'open', + }; + const errors = setSignalStatusValidateTypeDependents(schema); + expect(errors).toEqual(['either "signal_ids" or "query" must be set']); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_type_dependents.ts new file mode 100644 index 0000000000000..c495860fa72bf --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_type_dependents.ts @@ -0,0 +1,21 @@ +/* + * 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 { SetSignalsStatusSchema } from './set_signal_status_schema'; + +export const validateId = (signalStatus: SetSignalsStatusSchema): string[] => { + if (signalStatus.signal_ids != null && signalStatus.query != null) { + return ['both "signal_ids" and "query" cannot exist, choose one or the other']; + } else if (signalStatus.signal_ids == null && signalStatus.query == null) { + return ['either "signal_ids" or "query" must be set']; + } else { + return []; + } +}; + +export const setSignalStatusValidateTypeDependents = (schema: SetSignalsStatusSchema): string[] => { + return [...validateId(schema)]; +}; 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 new file mode 100644 index 0000000000000..edc652ce3b3f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts @@ -0,0 +1,277 @@ +/* + * 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 { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bulk_schema'; +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'; + +// only the basics of testing are here. +// see: update_rules_schema.test.ts for the bulk of the validation tests +// this just wraps updateRulesSchema in an array +describe('update_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + const payload: UpdateRulesBulkSchema = []; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(output.errors).toEqual([]); + expect(output.schema).toEqual([]); + }); + + test('made up values do not validate for a single element', () => { + const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }]; + + 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(output.schema).toEqual({}); + }); + + test('single array element does validate', () => { + const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock()]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([getUpdateRulesSchemaDecodedMock()]); + }); + + test('two array elements do validate', () => { + const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock(), getUpdateRulesSchemaMock()]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([ + getUpdateRulesSchemaDecodedMock(), + getUpdateRulesSchemaDecodedMock(), + ]); + }); + + test('single array element with a missing value (risk_score) will not validate', () => { + const singleItem = getUpdateRulesSchemaMock(); + delete singleItem.risk_score; + const payload: UpdateRulesBulkSchema = [singleItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => { + const singleItem = getUpdateRulesSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); + delete secondItem.risk_score; + const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => { + const singleItem = getUpdateRulesSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); + delete singleItem.risk_score; + const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where both are invalid (risk_score) will not validate', () => { + const singleItem = getUpdateRulesSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); + delete singleItem.risk_score; + delete secondItem.risk_score; + const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(output.schema).toEqual({}); + }); + + test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => { + const singleItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), + madeUpValue: 'something', + }; + const secondItem = getUpdateRulesSchemaMock(); + const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']); + expect(output.schema).toEqual({}); + }); + + test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => { + const singleItem: UpdateRulesSchema = getUpdateRulesSchemaMock(); + const secondItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), + madeUpValue: 'something', + }; + const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']); + expect(output.schema).toEqual({}); + }); + + test('two array elements where both are invalid (extra key and value) will not validate', () => { + const singleItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), + madeUpValue: 'something', + }; + const secondItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), + madeUpValue: 'something', + }; + const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue,madeUpValue"']); + 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]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['Invalid value "madeup" supplied to "severity"']); + expect(output.schema).toEqual({}); + }); + + test('You can set "note" to a string', () => { + const payload: UpdateRulesBulkSchema = [ + { ...getUpdateRulesSchemaMock(), note: '# test markdown' }, + ]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([ + { ...getUpdateRulesSchemaDecodedMock(), note: '# test markdown' }, + ]); + }); + + test('You can set "note" to an empty string', () => { + const payload: UpdateRulesBulkSchema = [{ ...getUpdateRulesSchemaMock(), note: '' }]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual([{ ...getUpdateRulesSchemaDecodedMock(), note: '' }]); + }); + + test('You can set "note" to anything other than string', () => { + const payload = [ + { + ...getUpdateRulesSchemaMock(), + note: { + something: 'some object', + }, + }, + ]; + + const decoded = updateRulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + // TODO: We should change the formatter used to better print objects + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "[object Object]" supplied to "note"', + ]); + 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 new file mode 100644 index 0000000000000..429103c7df13e --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { updateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema'; + +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 new file mode 100644 index 0000000000000..b8a99115ba7d5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts @@ -0,0 +1,42 @@ +/* + * 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 => ({ + 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', + 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 new file mode 100644 index 0000000000000..e60522e1964f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts @@ -0,0 +1,1387 @@ +/* + * 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'; + +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('[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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 ""']); + 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', () => { + 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 ""']); + 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 ""']); + 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 ""', + 'Invalid value "1" supplied to ""', + 'Invalid value "2" supplied to ""', + ]); + 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 "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 "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 "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 ""', + 'Invalid value "4" supplied to ""', + ]); + 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 "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 "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 "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 "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 "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([ + // TODO: Fix/Change the formatErrors to be better able to handle objects + 'Invalid value "[object Object]" 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 = { + 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); + }); + }); + + // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin + describe.skip('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + + test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + }); +}); 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 new file mode 100644 index 0000000000000..504233f95986f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts @@ -0,0 +1,140 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +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, + ThrottleOrNull, + note, + version, + References, + Actions, + Enabled, + FalsePositives, + From, + Interval, + language, + query, + id, +} from '../common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DefaultStringArray } from '../types/default_string_array'; +import { DefaultActionsArray } from '../types/default_actions_array'; +import { DefaultBooleanTrue } from '../types/default_boolean_true'; +import { DefaultFromString } from '../types/default_from_string'; +import { DefaultIntervalString } from '../types/default_interval_string'; +import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; +import { DefaultToString } from '../types/default_to_string'; +import { DefaultThreatArray } from '../types/default_threat_array'; +import { DefaultThrottleNull } from '../types/default_throttle_null'; +import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; + +/** + * 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 + enabled: DefaultBooleanTrue, // defaults to true 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, // 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 + // 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 + 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 + throttle: DefaultThrottleNull, // defaults to "null" 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: ListsDefaultArray, // defaults to empty array 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, + | 'references' + | 'actions' + | 'enabled' + | 'false_positives' + | 'from' + | 'interval' + | 'max_signals' + | 'tags' + | 'to' + | 'threat' + | 'throttle' + | 'exceptions_list' + | 'rule_id' +> & { + references: References; + actions: Actions; + enabled: Enabled; + false_positives: FalsePositives; + from: From; + interval: Interval; + max_signals: MaxSignals; + tags: Tags; + to: To; + threat: Threat; + throttle: ThrottleOrNull; + exceptions_list: ListsDefaultArraySchema; + 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 new file mode 100644 index 0000000000000..a63c8243cb5f1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { 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(), + timeline_id: '123', + }; + delete schema.timeline_title; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + timeline_id: '123', + timeline_title: '', + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_title" cannot be an empty string']); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + timeline_id: '', + timeline_title: 'some-title', + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['"timeline_id" cannot be an empty string']); + }); + + test('You cannot have timeline_title without timeline_id', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + timeline_title: 'some-title', + }; + delete schema.timeline_id; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); + }); + + test('You cannot have both an id and a rule_id', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + id: 'some-id', + rule_id: 'some-rule-id', + }; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['both "id" and "rule_id" cannot exist, choose one or the other']); + }); + + test('You must set either an id or a rule_id', () => { + const schema: UpdateRulesSchema = { + ...getUpdateRulesSchemaMock(), + }; + delete schema.id; + delete schema.rule_id; + const errors = updateRuleValidateTypeDependents(schema); + expect(errors).toEqual(['either "id" or "rule_id" must be set']); + }); +}); 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 new file mode 100644 index 0000000000000..9204f727b2660 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -0,0 +1,116 @@ +/* + * 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 } from './update_rules_schema'; + +export const validateAnomalyThreshold = (rule: UpdateRulesSchema): string[] => { + if (rule.type === 'machine_learning') { + 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 (rule.type === 'machine_learning') { + 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 (rule.type === 'machine_learning') { + 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 (rule.type === 'machine_learning') { + if (rule.machine_learning_job_id == null) { + return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; + } else { + return []; + } + } else { + return []; + } +}; + +export const validateTimelineId = (rule: UpdateRulesSchema): string[] => { + if (rule.timeline_id != null) { + if (rule.timeline_title == null) { + return ['when "timeline_id" exists, "timeline_title" must also exist']; + } else if (rule.timeline_id === '') { + return ['"timeline_id" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateTimelineTitle = (rule: UpdateRulesSchema): string[] => { + if (rule.timeline_title != null) { + if (rule.timeline_id == null) { + return ['when "timeline_title" exists, "timeline_id" must also exist']; + } else if (rule.timeline_title === '') { + return ['"timeline_title" cannot be an empty string']; + } else { + return []; + } + } + return []; +}; + +export const validateId = (rule: UpdateRulesSchema): string[] => { + if (rule.id != null && rule.rule_id != null) { + return ['both "id" and "rule_id" cannot exist, choose one or the other']; + } else if (rule.id == null && rule.rule_id == null) { + return ['either "id" or "rule_id" must be set']; + } else { + return []; + } +}; + +export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => { + return [ + ...validateId(schema), + ...validateAnomalyThreshold(schema), + ...validateQuery(schema), + ...validateLanguage(schema), + ...validateSavedId(schema), + ...validateMachineLearningJobId(schema), + ...validateTimelineId(schema), + ...validateTimelineTitle(schema), + ]; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts new file mode 100644 index 0000000000000..a2deaf626624f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { DefaultBooleanTrue } from './default_boolean_true'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_boolean_true', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default true', () => { + const payload = null; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts new file mode 100644 index 0000000000000..d68d447ca4454 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { DefaultFromString } from './default_from_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_from_string', () => { + test('it should validate a from string', () => { + const payload = 'now-20m'; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "now-6m"', () => { + const payload = null; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('now-6m'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts new file mode 100644 index 0000000000000..645eade71916f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { DefaultActionsArray } from './default_actions_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { Actions } from '../common/schemas'; + +describe('default_actions_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultActionsArray.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 actions', () => { + const payload: Actions = [ + { id: '123', group: 'group', action_type_id: 'action_type_id', params: {} }, + ]; + const decoded = DefaultActionsArray.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 = [ + { id: '123', group: 'group', action_type_id: 'action_type_id', params: {} }, + 5, + ]; + const decoded = DefaultActionsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultActionsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts new file mode 100644 index 0000000000000..ce3eb7fa7da83 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts @@ -0,0 +1,22 @@ +/* + * 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'; +import { actions, Actions } from '../common/schemas'; + +/** + * Types the DefaultStringArray as: + * - If null or undefined, then a default action array will be set + */ +export const DefaultActionsArray = new t.Type( + 'DefaultActionsArray', + actions.is, + (input): Either => (input == null ? t.success([]) : actions.decode(input)), + t.identity +); + +export type DefaultActionsArrayC = typeof DefaultActionsArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts new file mode 100644 index 0000000000000..1697928b17e0c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { DefaultBooleanFalse } from './default_boolean_false'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_boolean_false', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts new file mode 100644 index 0000000000000..624b9802f680c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts @@ -0,0 +1,22 @@ +/* + * 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'; + +/** + * Types the DefaultBooleanFalse as: + * - If null or undefined, then a default false will be set + */ +export const DefaultBooleanFalse = new t.Type( + 'DefaultBooleanFalse', + t.boolean.is, + (input): Either => + input == null ? t.success(false) : t.boolean.decode(input), + t.identity +); + +export type DefaultBooleanFalseC = typeof DefaultBooleanFalse; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts new file mode 100644 index 0000000000000..58c912a0a8650 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts @@ -0,0 +1,21 @@ +/* + * 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'; + +/** + * Types the DefaultBooleanTrue as: + * - If null or undefined, then a default true will be set + */ +export const DefaultBooleanTrue = new t.Type( + 'DefaultBooleanTrue', + t.boolean.is, + (input): Either => (input == null ? t.success(true) : t.boolean.decode(input)), + t.identity +); + +export type DefaultBooleanTrueC = typeof DefaultBooleanTrue; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts new file mode 100644 index 0000000000000..386d4c55905cd --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { DefaultEmptyString } from './default_empty_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_empty_string', () => { + test('it should validate a regular string', () => { + const payload = 'some string'; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of ""', () => { + const payload = null; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(''); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts new file mode 100644 index 0000000000000..6216d0c1111b0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts @@ -0,0 +1,21 @@ +/* + * 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'; + +/** + * Types the DefaultEmptyString as: + * - If null or undefined, then a default of an empty string "" will be used + */ +export const DefaultEmptyString = new t.Type( + 'DefaultEmptyString', + t.string.is, + (input): Either => (input == null ? t.success('') : t.string.decode(input)), + t.identity +); + +export type DefaultEmptyStringC = typeof DefaultEmptyString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts new file mode 100644 index 0000000000000..328cd738d7de0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { DefaultExportFileName } from './default_export_file_name'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_export_file_name', () => { + test('it should validate a regular string', () => { + const payload = 'some string'; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "export.ndjson"', () => { + const payload = null; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('export.ndjson'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts new file mode 100644 index 0000000000000..41dfdee1e0da0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts @@ -0,0 +1,22 @@ +/* + * 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'; + +/** + * Types the DefaultExportFileName as: + * - If null or undefined, then a default of "export.ndjson" will be used + */ +export const DefaultExportFileName = new t.Type( + 'DefaultExportFileName', + t.string.is, + (input): Either => + input == null ? t.success('export.ndjson') : t.string.decode(input), + t.identity +); + +export type DefaultExportFileNameC = typeof DefaultExportFileName; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts new file mode 100644 index 0000000000000..4217532de954e --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts @@ -0,0 +1,22 @@ +/* + * 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'; + +/** + * Types the DefaultFromString as: + * - If null or undefined, then a default of the string "now-6m" will be used + */ +export const DefaultFromString = new t.Type( + 'DefaultFromString', + t.string.is, + (input): Either => + input == null ? t.success('now-6m') : t.string.decode(input), + t.identity +); + +export type DefaultFromStringC = typeof DefaultFromString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts new file mode 100644 index 0000000000000..9720178a4ae9b --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { DefaultIntervalString } from './default_interval_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_interval_string', () => { + test('it should validate a interval string', () => { + const payload = '20m'; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "5m"', () => { + const payload = null; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('5m'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts new file mode 100644 index 0000000000000..579e7591fdb03 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts @@ -0,0 +1,21 @@ +/* + * 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'; + +/** + * Types the DefaultIntervalString as: + * - If null or undefined, then a default of the string "5m" will be used + */ +export const DefaultIntervalString = new t.Type( + 'DefaultIntervalString', + t.string.is, + (input): Either => (input == null ? t.success('5m') : t.string.decode(input)), + t.identity +); + +export type DefaultIntervalStringC = typeof DefaultIntervalString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts new file mode 100644 index 0000000000000..e3da8dbd280ab --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { DefaultLanguageString } from './default_language_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { Language } from '../common/schemas'; + +describe('default_language_string', () => { + test('it should validate a string', () => { + const payload: Language = 'lucene'; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "kuery"', () => { + const payload = null; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('kuery'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts new file mode 100644 index 0000000000000..248e15d56dfd7 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts @@ -0,0 +1,23 @@ +/* + * 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'; +import { language } from '../common/schemas'; + +/** + * Types the DefaultLanguageString as: + * - If null or undefined, then a default of the string "kuery" will be used + */ +export const DefaultLanguageString = new t.Type( + 'DefaultLanguageString', + t.string.is, + (input): Either => + input == null ? t.success('kuery') : language.decode(input), + t.identity +); + +export type DefaultLanguageStringC = typeof DefaultLanguageString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts new file mode 100644 index 0000000000000..a6f137c3f2113 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { DefaultMaxSignalsNumber } from './default_max_signals_number'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +describe('default_from_string', () => { + test('it should validate a max signal number', () => { + const payload = 5; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of DEFAULT_MAX_SIGNALS', () => { + const payload = null; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts new file mode 100644 index 0000000000000..6f0c32c5466f3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts @@ -0,0 +1,32 @@ +/* + * 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'; +// eslint-disable-next-line @typescript-eslint/camelcase +import { max_signals } from '../common/schemas'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; + +/** + * Types the default max signal: + * - Natural Number (positive integer and not a float), + * - greater than 1 + * - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default + */ +export const DefaultMaxSignalsNumber: DefaultMaxSignalsNumberC = new t.Type< + number, + number, + unknown +>( + 'DefaultMaxSignals', + t.number.is, + (input): Either => { + return input == null ? t.success(DEFAULT_MAX_SIGNALS) : max_signals.decode(input); + }, + t.identity +); + +export type DefaultMaxSignalsNumberC = t.Type; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts new file mode 100644 index 0000000000000..1d1d43667c710 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DefaultPage } from './default_page'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_page', () => { + test('it should validate a regular number greater than zero', () => { + const payload = 5; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a string of a number', () => { + const payload = '5'; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(5); + }); + + test('it should not validate a junk string', () => { + const payload = 'invalid-string'; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 20', () => { + const payload = null; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts new file mode 100644 index 0000000000000..95e3b42f3e138 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts @@ -0,0 +1,32 @@ +/* + * 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'; +import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; + +/** + * Types the DefaultPerPage as: + * - If a string this will convert the string to a number + * - If null or undefined, then a default of 1 will be used + * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + */ +export const DefaultPage = new t.Type( + 'DefaultPerPage', + t.number.is, + (input): Either => { + if (input == null) { + return t.success(1); + } else if (typeof input === 'string') { + return PositiveIntegerGreaterThanZero.decode(parseInt(input, 10)); + } else { + return PositiveIntegerGreaterThanZero.decode(input); + } + }, + t.identity +); + +export type DefaultPageC = typeof DefaultPage; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts new file mode 100644 index 0000000000000..3ecbae6ed43f5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DefaultPerPage } from './default_per_page'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_per_page', () => { + test('it should validate a regular number greater than zero', () => { + const payload = 5; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a string of a number', () => { + const payload = '5'; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(5); + }); + + test('it should not validate a junk string', () => { + const payload = 'invalid-string'; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 20', () => { + const payload = null; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(20); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts new file mode 100644 index 0000000000000..f96f280f6af11 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts @@ -0,0 +1,32 @@ +/* + * 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'; +import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; + +/** + * Types the DefaultPerPage as: + * - If a string this will convert the string to a number + * - If null or undefined, then a default of 20 will be used + * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + */ +export const DefaultPerPage = new t.Type( + 'DefaultPerPage', + t.number.is, + (input): Either => { + if (input == null) { + return t.success(20); + } else if (typeof input === 'string') { + return PositiveIntegerGreaterThanZero.decode(parseInt(input, 10)); + } else { + return PositiveIntegerGreaterThanZero.decode(input); + } + }, + t.identity +); + +export type DefaultPerPageC = typeof DefaultPerPage; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts new file mode 100644 index 0000000000000..83142c8d65777 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { DefaultStringArray } from './default_string_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_string_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultStringArray.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 strings', () => { + const payload = ['value 1', 'value 2']; + const decoded = DefaultStringArray.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 = ['value 1', 5]; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_string_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts similarity index 75% rename from x-pack/plugins/lists/common/schemas/types/default_string_array.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts index c6ebffa379903..1f043cfd1b8e5 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_string_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts @@ -7,16 +7,16 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type DefaultStringArrayC = t.Type; - /** * Types the DefaultStringArray as: * - If null or undefined, then a default array will be set */ -export const DefaultStringArray: DefaultStringArrayC = new t.Type( - 'DefaultArray', +export const DefaultStringArray = new t.Type( + 'DefaultStringArray', t.array(t.string).is, (input): Either => input == null ? t.success([]) : t.array(t.string).decode(input), t.identity ); + +export type DefaultStringArrayC = typeof DefaultStringArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts new file mode 100644 index 0000000000000..1941a642e8baf --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { DefaultStringBooleanFalse } from './default_string_boolean_false'; + +describe('default_string_boolean_false', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); + + test('it should return a default false when given a string of "false"', () => { + const payload = 'false'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); + + test('it should return a default true when given a string of "true"', () => { + const payload = 'true'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); + + test('it should return a default true when given a string of "TruE"', () => { + const payload = 'TruE'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); + + test('it should not work with a strong of junk "junk"', () => { + const payload = 'junk'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not work with an empty string', () => { + const payload = ''; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to ""']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts new file mode 100644 index 0000000000000..48a40d4b9ceec --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts @@ -0,0 +1,32 @@ +/* + * 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'; + +/** + * Types the DefaultStringBooleanFalse as: + * - If a string this will convert the string to a boolean + * - If null or undefined, then a default false will be set + */ +export const DefaultStringBooleanFalse = new t.Type( + 'DefaultStringBooleanFalse', + t.boolean.is, + (input): Either => { + if (input == null) { + return t.success(false); + } else if (typeof input === 'string' && input.toLowerCase() === 'true') { + return t.success(true); + } else if (typeof input === 'string' && input.toLowerCase() === 'false') { + return t.success(false); + } else { + return t.boolean.decode(input); + } + }, + t.identity +); + +export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts new file mode 100644 index 0000000000000..9819da0b8d463 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { DefaultThreatArray } from './default_threat_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { Threat } from '../common/schemas'; + +describe('default_threat_null', () => { + test('it should validate an empty array', () => { + const payload: Threat = []; + const decoded = DefaultThreatArray.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 threats', () => { + const payload: Threat = [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ]; + const decoded = DefaultThreatArray.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 = [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + 5, + ]; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default empty array if not provided a value', () => { + const payload = null; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts new file mode 100644 index 0000000000000..da0611e24bc7e --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts @@ -0,0 +1,22 @@ +/* + * 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'; +import { Threat, threat } from '../common/schemas'; + +/** + * Types the DefaultThreatArray as: + * - If null or undefined, then an empty array will be set + */ +export const DefaultThreatArray = new t.Type( + 'DefaultThreatArray', + threat.is, + (input): Either => (input == null ? t.success([]) : threat.decode(input)), + t.identity +); + +export type DefaultThreatArrayC = typeof DefaultThreatArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts new file mode 100644 index 0000000000000..304fd65647c3c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { DefaultThrottleNull } from './default_throttle_null'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { Throttle } from '../common/schemas'; + +describe('default_throttle_null', () => { + test('it should validate a throttle string', () => { + const payload: Throttle = 'some string'; + const decoded = DefaultThrottleNull.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 = 5; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default "null" if not provided a value', () => { + const payload = undefined; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts new file mode 100644 index 0000000000000..fd31594323f4d --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts @@ -0,0 +1,23 @@ +/* + * 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'; +import { ThrottleOrNull, throttle } from '../common/schemas'; + +/** + * Types the DefaultThrottleNull as: + * - If null or undefined, then a null will be set + */ +export const DefaultThrottleNull = new t.Type( + 'DefaultThreatNull', + throttle.is, + (input): Either => + input == null ? t.success(null) : throttle.decode(input), + t.identity +); + +export type DefaultThrottleNullC = typeof DefaultThrottleNull; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts new file mode 100644 index 0000000000000..3e22d57cedf99 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { DefaultToString } from './default_to_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_to_string', () => { + test('it should validate a to string', () => { + const payload = 'now-5m'; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "now"', () => { + const payload = null; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('now'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts new file mode 100644 index 0000000000000..163bcf8c4e5b2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts @@ -0,0 +1,21 @@ +/* + * 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'; + +/** + * Types the DefaultToString as: + * - If null or undefined, then a default of the string "now" will be used + */ +export const DefaultToString = new t.Type( + 'DefaultFromString', + t.string.is, + (input): Either => (input == null ? t.success('now') : t.string.decode(input)), + t.identity +); + +export type DefaultToStringC = typeof DefaultToString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts new file mode 100644 index 0000000000000..7dab8869d5d87 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { DefaultUuid } from './default_uuid'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).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 + ); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts similarity index 84% rename from x-pack/plugins/lists/common/schemas/types/default_uuid.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts index 4fb4133d7353f..b0c328a93ff03 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_uuid.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts @@ -10,17 +10,17 @@ import uuid from 'uuid'; import { NonEmptyString } from './non_empty_string'; -export type DefaultUuidC = t.Type; - /** * Types the DefaultUuid as: * - If null or undefined, then a default string uuid.v4() will be * created otherwise it will be checked just against an empty string */ -export const DefaultUuid: DefaultUuidC = new t.Type( +export const DefaultUuid = new t.Type( 'DefaultUuid', t.string.is, (input): Either => input == null ? t.success(uuid.v4()) : NonEmptyString.decode(input), t.identity ); + +export type DefaultUuidC = typeof DefaultUuid; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts new file mode 100644 index 0000000000000..65697d8830b66 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { DefaultVersionNumber } from './default_version_number'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('default_version_number', () => { + test('it should validate a version number', () => { + const payload = 5; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a 0', () => { + const payload = 0; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a -1', () => { + const payload = -1; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 1', () => { + const payload = null; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts new file mode 100644 index 0000000000000..4a310329660df --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts @@ -0,0 +1,22 @@ +/* + * 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'; +import { version, Version } from '../common/schemas'; + +/** + * Types the DefaultVersionNumber as: + * - If null or undefined, then a default of the number 1 will be used + */ +export const DefaultVersionNumber = new t.Type( + 'DefaultVersionNumber', + version.is, + (input): Either => (input == null ? t.success(1) : version.decode(input)), + t.identity +); + +export type DefaultVersionNumberC = typeof DefaultVersionNumber; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts index d63c18154c5d4..4e12a45f21d0b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts @@ -7,13 +7,11 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type IsoDateStringC = t.Type; - /** * Types the IsoDateString as: * - A string that is an ISOString */ -export const IsoDateString: IsoDateStringC = new t.Type( +export const IsoDateString = new t.Type( 'IsoDateString', t.string.is, (input, context): Either => { @@ -34,3 +32,5 @@ export const IsoDateString: IsoDateStringC = new t.Type }, t.identity ); + +export type IsoDateStringC = typeof IsoDateString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts index 8244f4a29e193..8cdd865469112 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts @@ -13,7 +13,6 @@ import { list_values_operator as listOperator, } from '../common/schemas'; -export type ListsDefaultArrayC = t.Type; export type List = t.TypeOf; export type ListValues = t.TypeOf; export type ListOperator = t.TypeOf; @@ -22,7 +21,7 @@ export type ListOperator = t.TypeOf; * Types the ListsDefaultArray as: * - If null or undefined, then a default array will be set for the list */ -export const ListsDefaultArray: ListsDefaultArrayC = new t.Type( +export const ListsDefaultArray = new t.Type( 'listsWithDefaultArray', t.array(listAnd).is, (input): Either => @@ -30,4 +29,6 @@ export const ListsDefaultArray: ListsDefaultArrayC = new t.Type; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts new file mode 100644 index 0000000000000..0a88b87421e70 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { NonEmptyString } from './non_empty_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +describe('non_empty_string', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate empty spaces', () => { + const payload = ' '; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value " " supplied to ""']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts similarity index 81% rename from x-pack/plugins/lists/common/schemas/types/non_empty_string.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts index d1e2094bbcad3..b3fad548932d5 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts @@ -7,13 +7,11 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type NonEmptyStringC = t.Type; - /** * Types the NonEmptyString as: * - A string that is not empty */ -export const NonEmptyString: NonEmptyStringC = new t.Type( +export const NonEmptyString = new t.Type( 'NonEmptyString', t.string.is, (input, context): Either => { @@ -25,3 +23,5 @@ export const NonEmptyString: NonEmptyStringC = new t.Type { + test('it should validate a boolean false as false', () => { + const payload = false; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a boolean true', () => { + const payload = true; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "true" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts new file mode 100644 index 0000000000000..b22562e2ab9dc --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts @@ -0,0 +1,33 @@ +/* + * 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'; + +/** + * Types the OnlyFalseAllowed as: + * - If null or undefined, then a default false will be set + * - If true is sent in then this will return an error + * - If false is sent in then this will allow it only false + */ +export const OnlyFalseAllowed = new t.Type( + 'DefaultBooleanTrue', + t.boolean.is, + (input, context): Either => { + if (input == null) { + return t.success(false); + } else { + if (typeof input === 'boolean' && input === false) { + return t.success(false); + } else { + return t.failure(input, context); + } + } + }, + t.identity +); + +export type OnlyFalseAllowedC = typeof OnlyFalseAllowed; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts index ac98dd6f5d85c..298487a3fae98 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts @@ -7,14 +7,12 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type PositiveIntegerC = t.Type; - /** * Types the positive integer are: * - Natural Number (positive integer and not a float), * - zero or greater */ -export const PositiveInteger: PositiveIntegerC = new t.Type( +export const PositiveInteger = new t.Type( 'PositiveInteger', t.number.is, (input, context): Either => { @@ -24,3 +22,5 @@ export const PositiveInteger: PositiveIntegerC = new t.Type; - /** * Types the positive integer greater than zero is: * - Natural Number (positive integer and not a float), * - 1 or greater */ -export const PositiveIntegerGreaterThanZero: PositiveIntegerGreaterThanZeroC = new t.Type< - number, - number, - unknown ->( +export const PositiveIntegerGreaterThanZero = new t.Type( 'PositiveIntegerGreaterThanZero', t.number.is, (input, context): Either => { @@ -28,3 +22,5 @@ export const PositiveIntegerGreaterThanZero: PositiveIntegerGreaterThanZeroC = n }, t.identity ); + +export type PositiveIntegerGreaterThanZeroC = typeof PositiveIntegerGreaterThanZero; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts index 43e2dbdac1fe1..83142c8d65777 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReferencesDefaultArray } from './references_default_array'; +import { DefaultStringArray } from './default_string_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../../test_utils'; -describe('references_default_array', () => { +describe('default_string_array', () => { test('it should validate an empty array', () => { const payload: string[] = []; - const decoded = ReferencesDefaultArray.decode(payload); + const decoded = DefaultStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -21,7 +21,7 @@ describe('references_default_array', () => { test('it should validate an array of strings', () => { const payload = ['value 1', 'value 2']; - const decoded = ReferencesDefaultArray.decode(payload); + const decoded = DefaultStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -30,7 +30,7 @@ describe('references_default_array', () => { test('it should not validate an array with a number', () => { const payload = ['value 1', 5]; - const decoded = ReferencesDefaultArray.decode(payload); + const decoded = DefaultStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); @@ -39,7 +39,7 @@ describe('references_default_array', () => { test('it should return a default array entry', () => { const payload = null; - const decoded = ReferencesDefaultArray.decode(payload); + const decoded = DefaultStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts index 57a7ed4e4d456..b809181ce8c32 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts @@ -7,20 +7,16 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type ReferencesDefaultArrayC = t.Type; - /** * Types the ReferencesDefaultArray as: * - If null or undefined, then a default array will be set */ -export const ReferencesDefaultArray: ReferencesDefaultArrayC = new t.Type< - string[], - string[], - unknown ->( +export const ReferencesDefaultArray = new t.Type( 'referencesWithDefaultArray', t.array(t.string).is, (input): Either => input == null ? t.success([]) : t.array(t.string).decode(input), t.identity ); + +export type ReferencesDefaultArrayC = typeof ReferencesDefaultArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts index c44fae3c6f3e7..7f057a6d178e1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts @@ -7,14 +7,12 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type RiskScoreC = t.Type; - /** * Types the risk score as: * - Natural Number (positive integer and not a float), * - Between the values [0 and 100] inclusive. */ -export const RiskScore: RiskScoreC = new t.Type( +export const RiskScore = new t.Type( 'RiskScore', t.number.is, (input, context): Either => { @@ -24,3 +22,5 @@ export const RiskScore: RiskScoreC = new t.Type( }, t.identity ); + +export type RiskScoreC = typeof RiskScore; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts index 88e9db5964198..3676aa5686cd6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts @@ -7,8 +7,6 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -export type UUIDC = t.Type; - const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** @@ -16,7 +14,7 @@ const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; * - Natural Number (positive integer and not a float), * - Between the values [0 and 100] inclusive. */ -export const UUID: UUIDC = new t.Type( +export const UUID = new t.Type( 'UUID', t.string.is, (input, context): Either => { @@ -26,3 +24,5 @@ export const UUID: UUIDC = new t.Type( }, t.identity ); + +export type UUIDC = typeof UUID; diff --git a/x-pack/plugins/security_solution/common/exact_check.test.ts b/x-pack/plugins/security_solution/common/exact_check.test.ts index a5701603de0d6..96a1b1266ea9a 100644 --- a/x-pack/plugins/security_solution/common/exact_check.test.ts +++ b/x-pack/plugins/security_solution/common/exact_check.test.ts @@ -86,6 +86,23 @@ describe('exact_check', () => { expect(message.schema).toEqual({ a: 'test' }); }); + test('it will work with decoding a null payload when the schema expects a null', () => { + const someType = t.union([ + t.exact( + t.type({ + a: t.string, + }) + ), + t.null, + ]); + const payload = null; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(null); + }); + test('it should find no differences recursively with two empty objects', () => { const difference = findDifferencesRecursive({}, {}); expect(difference).toEqual([]); @@ -151,4 +168,9 @@ describe('exact_check', () => { ); expect(difference).toEqual(['d']); }); + + test('it should not find any differences when the original and decoded are both null', () => { + const difference = findDifferencesRecursive(null, null); + expect(difference).toEqual([]); + }); }); diff --git a/x-pack/plugins/security_solution/common/exact_check.ts b/x-pack/plugins/security_solution/common/exact_check.ts index 30c5b585a3480..48afc35b56ba1 100644 --- a/x-pack/plugins/security_solution/common/exact_check.ts +++ b/x-pack/plugins/security_solution/common/exact_check.ts @@ -25,7 +25,10 @@ import { isObject, get } from 'lodash/fp'; * @param decoded The decoded either which has either an existing error or the * decoded object which could have additional keys stripped from it. */ -export const exactCheck = (original: T, decoded: Either): Either => { +export const exactCheck = ( + original: unknown, + decoded: Either +): Either => { const onLeft = (errors: t.Errors): Either => left(errors); const onRight = (decodedValue: T): Either => { const differences = findDifferencesRecursive(original, decodedValue); @@ -45,7 +48,11 @@ export const exactCheck = (original: T, decoded: Either): Either }; export const findDifferencesRecursive = (original: T, decodedValue: T): string[] => { - if (decodedValue == null) { + if (decodedValue === null && original === null) { + // both the decodedValue and the original are null which indicates that they are equal + // so do not report differences + return []; + } else if (decodedValue == null) { try { // It is null and painful when the original contains an object or an array // the the decoded value does not have. diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx index 3eedbcc97bbd1..5a08b0a20d1fc 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx @@ -13,6 +13,8 @@ import { StepAboutRule } from '.'; import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; import { StepRuleDescription } from '../description_step'; import { stepAboutDefaultValue } from './default_value'; +import { wait } from '@testing-library/react'; +import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; const theme = () => ({ eui: euiDarkVars, darkMode: true }); @@ -57,31 +59,31 @@ describe('StepAboutRuleComponent', () => { ); - const nameInput = wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0); - nameInput.simulate('change', { target: { value: 'Test name text' } }); + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') + .first() + .simulate('change', { target: { value: 'Test name text' } }); const descriptionInput = wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0); - const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); - nextButton.simulate('click'); + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first(); + wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click'); expect( - wrapper.find('input[aria-describedby="detectionEngineStepAboutRuleName"]').at(0).props().value + wrapper.find('[data-test-subj="detectionEngineStepAboutRuleName"] input').first().props() + .value ).toEqual('Test name text'); expect(descriptionInput.props().value).toEqual(''); expect( wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] label') - .at(0) + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] label') + .first() .hasClass('euiFormLabel-isInvalid') ).toBeTruthy(); expect( wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') - .at(0) + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') + .first() .prop('isInvalid') ).toBeTruthy(); }); @@ -101,39 +103,40 @@ describe('StepAboutRuleComponent', () => { ); - const descriptionInput = wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0); - descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first() + .simulate('change', { target: { value: 'Test description text' } }); const nameInput = wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0); - const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); - nextButton.simulate('click'); + .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') + .first(); + + wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click'); expect( wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0) + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first() .props().value ).toEqual('Test description text'); expect(nameInput.props().value).toEqual(''); expect( wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] label') - .at(0) + .find('[data-test-subj="detectionEngineStepAboutRuleName"] label') + .first() .hasClass('euiFormLabel-isInvalid') ).toBeTruthy(); expect( wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') - .at(0) + .find('[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') + .first() .prop('isInvalid') ).toBeTruthy(); }); - test('it allows user to click continue if "name" and "description" are defined', () => { + test('it allows user to click continue if "name" and "description" are defined', async () => { + const stepDataMock = jest.fn(); const wrapper = mount( { isReadOnlyView={false} isLoading={false} setForm={jest.fn()} - setStepData={jest.fn()} + setStepData={stepDataMock} /> ); - const descriptionInput = wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0); - descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first() + .simulate('change', { target: { value: 'Test description text' } }); + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') + .first() + .simulate('change', { target: { value: 'Test name text' } }); + + wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click').update(); + await wait(); + const expected: Omit = { + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: 50, + severity: 'low', + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + }); - const nameInput = wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0); - nameInput.simulate('change', { target: { value: 'Test name text' } }); + test('it allows user to set the risk score as a number (and not a string)', async () => { + const stepDataMock = jest.fn(); + const wrapper = mount( + + + + ); - const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); - nextButton.simulate('click'); + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleName"] input') + .first() + .simulate('change', { target: { value: 'Test name text' } }); + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea') + .first() + .simulate('change', { target: { value: 'Test description text' } }); + + wrapper + .find('[data-test-subj="detectionEngineStepAboutRuleRiskScore"] input') + .first() + .simulate('change', { target: { value: '80' } }); + + wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); + await wait(); + const expected: Omit = { + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: 80, + severity: 'low', + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + expect(stepDataMock.mock.calls[1][1]).toEqual(expected); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx index 42ffab10ff469..59ecebaeb9e4e 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx @@ -87,6 +87,7 @@ export const schema: FormSchema = { }, riskScore: { type: FIELD_TYPES.RANGE, + serializer: (input: string) => Number(input), label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', { 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 9246d8cb3519b..23d5d7eb8d385 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 @@ -6,11 +6,7 @@ import { SavedObjectsFindResponse } from 'kibana/server'; import { ActionResult } from '../../../../../../actions/server'; -import { - SignalsStatusRestParams, - SignalsQueryRestParams, - SignalSearchResponse, -} from '../../signals/types'; +import { SignalSearchResponse } from '../../signals/types'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -26,45 +22,11 @@ import { IRuleSavedAttributesSavedObjectAttributes, HapiReadableStream, } from '../../rules/types'; -import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; +import { RuleAlertParamsRest } from '../../types'; import { requestMock } from './request'; import { RuleNotificationAlertType } from '../../notifications/types'; - -export const mockPrepackagedRule = (): PrepackagedRules => ({ - rule_id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - risk_score: 50, - type: 'query', - from: 'now-6m', - to: 'now', - severity: 'high', - query: 'user.name: root or user.name: admin', - language: 'kuery', - threat: [ - { - framework: 'fake', - tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, - technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], - }, - ], - throttle: null, - enabled: true, - filters: [], - immutable: false, - references: [], - meta: {}, - tags: [], - version: 1, - false_positives: [], - max_signals: 100, - note: '', - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', -}); +import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; +import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; export const typicalPayload = (): Partial => ({ rule_id: 'rule-1', @@ -89,25 +51,25 @@ export const typicalPayload = (): Partial => ({ ], }); -export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ +export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], status: 'closed', }); -export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ +export const typicalSetStatusSignalByQueryPayload = (): SetSignalsStatusSchemaDecoded => ({ query: { bool: { filter: { range: { '@timestamp': { gte: 'now-2M', lte: 'now/M' } } } } }, status: 'closed', }); -export const typicalSignalsQuery = (): Partial => ({ +export const typicalSignalsQuery = (): QuerySignalsSchemaDecoded => ({ query: { match_all: {} }, }); -export const typicalSignalsQueryAggs = (): Partial => ({ +export const typicalSignalsQueryAggs = (): QuerySignalsSchemaDecoded => ({ aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, }); -export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ +export const setStatusSignalMissingIdsAndQueryPayload = (): SetSignalsStatusSchemaDecoded => ({ status: 'closed', }); @@ -170,14 +132,14 @@ export const getDeleteBulkRequestById = () => requestMock.create({ method: 'delete', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - body: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], + body: [{ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], }); export const getDeleteAsPostBulkRequestById = () => requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, - body: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], + body: [{ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], }); export const getDeleteAsPostBulkRequest = () => @@ -448,7 +410,7 @@ export const getResult = (): RuleAlertType => ({ references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, - exceptions_list: [ + exceptionsList: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 241d362753d53..4b65ee5efdff0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -13,18 +13,16 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock } from '../__mocks__'; import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; -import { PrepackagedRules } from '../../types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; jest.mock('../../rules/get_prepackaged_rules', () => { return { - getPrepackagedRules: (): PrepackagedRules[] => { + getPrepackagedRules: (): AddPrepackagedRulesSchemaDecoded[] => { return [ { tags: [], - immutable: true, rule_id: 'rule-1', - output_index: '.siem-signals', risk_score: 50, description: 'some description', from: 'now-5m', @@ -34,6 +32,16 @@ jest.mock('../../rules/get_prepackaged_rules', () => { severity: 'low', interval: '5m', type: 'query', + query: 'user.name: root or user.name: admin', + language: 'kuery', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: 100, + threat: [], + throttle: null, + exceptions_list: [], version: 2, // set one higher than the mocks which is set to 1 to trigger updates }, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index d5628877ee16e..39eea16c6290a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -29,7 +29,7 @@ export const addPrepackedRulesRoute = (router: IRouter) => { tags: ['access:securitySolution'], }, }, - async (context, request, response) => { + async (context, _, response) => { const siemResponse = buildSiemResponse(response); try { 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 6875b0fc76ecc..1df6070bc33a6 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 @@ -180,7 +180,7 @@ describe('create_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' + 'Invalid value "unexpected_type" supplied to "type"' ); }); }); 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 dc0b65cef61d0..7af58adca7529 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 @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import uuid from 'uuid'; - +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 { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -13,26 +17,24 @@ import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; -import { RuleAlertParamsRest } from '../../types'; import { readRules } from '../../rules/read_rules'; import { getDuplicates } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; -import { - transformBulkError, - createBulkErrorObject, - buildRouteValidation, - buildSiemResponse, -} from '../utils'; -import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; + +import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { PartialFilter } from '../../types'; 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'], @@ -59,19 +61,19 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => .filter((rule) => rule.rule_id == null || !dupes.includes(rule.rule_id)) .map(async (payloadRule) => { const { - actions, + actions: actionsRest, anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, meta, - filters, + filters: filtersRest, rule_id: ruleId, index, interval, @@ -89,23 +91,42 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => timeline_id: timelineId, timeline_title: timelineTitle, version, - exceptions_list, + exceptions_list: exceptionsList, } = payloadRule; - const ruleIdOrUuid = ruleId ?? uuid.v4(); try { + const validationErrors = createRuleValidateTypeDependents(payloadRule); + if (validationErrors.length) { + return createBulkErrorObject({ + ruleId, + statusCode: 400, + message: validationErrors.join(), + }); + } + + const query = + type !== 'machine_learning' && queryOrUndefined == null ? '' : queryOrUndefined; + + const language = + type !== 'machine_learning' && 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(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ - ruleId: ruleIdOrUuid, + 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 }); + const rule = await readRules({ alertsClient, ruleId, id: undefined }); if (rule != null) { return createBulkErrorObject({ ruleId, @@ -131,7 +152,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => timelineTitle, meta, filters, - ruleId: ruleIdOrUuid, + ruleId, index, interval, maxSignals, @@ -145,7 +166,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => references, note, version, - exceptions_list, + exceptionsList, actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it, }); @@ -159,9 +180,9 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => name, }); - return transformValidateBulkError(ruleIdOrUuid, createdRule, ruleActions); + return transformValidateBulkError(ruleId, createdRule, ruleActions); } catch (err) { - return transformBulkError(ruleIdOrUuid, err); + return transformBulkError(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 7984b84729821..5abd7b1e76a76 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 @@ -170,7 +170,7 @@ describe('create_rules', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' + 'Invalid value "unexpected_type" supplied to "type"' ); }); }); 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 6138db4dcb0ff..78d67e0e9366c 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,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import uuid from 'uuid'; - +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'; @@ -13,42 +18,48 @@ 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 { RuleAlertParamsRest } from '../../types'; import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; -import { createRulesSchema } from '../schemas/create_rules_schema'; -import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; +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'; 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'], }, }, async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + const validationErrors = createRuleValidateTypeDependents(request.body); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } const { - actions, + actions: actionsRest, anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, timeline_title: timelineTitle, meta, machine_learning_job_id: machineLearningJobId, - filters, + filters: filtersRest, rule_id: ruleId, index, interval, @@ -63,11 +74,21 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void type, references, note, - exceptions_list, + exceptions_list: exceptionsList, } = request.body; - const siemResponse = buildSiemResponse(response); - try { + const query = + type !== 'machine_learning' && queryOrUndefined == null ? '' : queryOrUndefined; + + const language = + type !== 'machine_learning' && 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; @@ -89,7 +110,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }); } if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId }); + const rule = await readRules({ alertsClient, ruleId, id: undefined }); if (rule != null) { return siemResponse.error({ statusCode: 409, @@ -114,7 +135,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void meta, machineLearningJobId, filters, - ruleId: ruleId ?? uuid.v4(), + ruleId, index, interval, maxSignals, @@ -128,7 +149,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void references, note, version: 1, - exceptions_list, + exceptionsList, actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index f2da3ab4be8f6..4c1c1046a9fcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -105,11 +105,33 @@ describe('delete_rules', () => { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, body: [{}], }); - const result = server.validate(request); + const response = await server.inject(request, context); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + error: { message: 'either "id" or "rule_id" must be set', status_code: 400 }, + rule_id: '(unknown id)', + }, + ]); + }); - expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because ["value" must contain at least one of [id, rule_id]]' - ); + test('rejects requests with both id and rule_id', async () => { + const request = requestMock.create({ + method: 'post', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + body: [{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f', rule_id: 'rule_1' }], + }); + const response = await server.inject(request, context); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + error: { + message: 'both "id" and "rule_id" cannot exist, choose one or the other', + status_code: 400, + }, + rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f', + }, + ]); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index c1e359b36caa1..12f908ce7e8b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -4,26 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { + queryRulesBulkSchema, + QueryRulesBulkSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/query_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter, RouteConfig, RequestHandler } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; -import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; -import { DeleteRulesRequestParams } from '../../rules/types'; +import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { deleteRules } from '../../rules/delete_rules'; import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -type Config = RouteConfig; -type Handler = RequestHandler; +type Config = RouteConfig; +type Handler = RequestHandler; export const deleteRulesBulkRoute = (router: IRouter) => { const config: Config = { validate: { - body: buildRouteValidation(queryRulesBulkSchema), + body: buildRouteValidation( + queryRulesBulkSchema + ), }, path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, options: { @@ -46,6 +52,15 @@ export const deleteRulesBulkRoute = (router: IRouter) => { request.body.map(async (payloadRule) => { const { id, rule_id: ruleId } = payloadRule; const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + const validationErrors = queryRuleValidateTypeDependents(payloadRule); + if (validationErrors.length) { + return createBulkErrorObject({ + ruleId: idOrRuleIdOrUnknown, + statusCode: 400, + message: validationErrors.join(), + }); + } + try { const rule = await deleteRules({ alertsClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index e30f332ecd1ca..8530b845e9bb9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -92,11 +92,12 @@ describe('delete_rules', () => { path: DETECTION_ENGINE_RULES_URL, query: {}, }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith( - '"value" must contain at least one of [id, rule_id]' - ); + const response = await server.inject(request, context); + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: ['either "id" or "rule_id" must be set'], + status_code: 400, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index a2256b8338d2f..f4aa51c6dcfc3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -4,14 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; +import { + queryRulesSchema, + QueryRulesSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/query_rules_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; -import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { DeleteRuleRequestParams } from '../../rules/types'; +import { transformError, buildSiemResponse } from '../utils'; import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; @@ -21,7 +25,9 @@ export const deleteRulesRoute = (router: IRouter) => { { path: DETECTION_ENGINE_RULES_URL, validate: { - query: buildRouteValidation(queryRulesSchema), + query: buildRouteValidation( + queryRulesSchema + ), }, options: { tags: ['access:securitySolution'], @@ -29,6 +35,10 @@ export const deleteRulesRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + const validationErrors = queryRuleValidateTypeDependents(request.query); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } try { const { id, rule_id: ruleId } = request.query; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index ad0b3c87ff145..8df9f114559a8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -4,23 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + exportRulesQuerySchema, + ExportRulesQuerySchemaDecoded, + exportRulesSchema, + ExportRulesSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/export_rules_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { ConfigType } from '../../../../config'; -import { ExportRulesRequestParams } from '../../rules/types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; -import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; -import { transformError, buildRouteValidation, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; export const exportRulesRoute = (router: IRouter, config: ConfigType) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_export`, validate: { - query: buildRouteValidation(exportRulesQuerySchema), - body: buildRouteValidation(exportRulesSchema), + query: buildRouteValidation( + exportRulesQuerySchema + ), + body: buildRouteValidation( + exportRulesSchema + ), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index b4591a8141f7b..892b7a2dd7315 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -91,7 +91,7 @@ describe('find_rules', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith('"invalid_value" is not allowed'); + expect(result.badRequest).toHaveBeenCalledWith('invalid keys "invalid_value"'); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 7d99be908d064..eceb953762090 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -4,22 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { findRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/find_rules_type_dependents'; +import { + findRulesSchema, + FindRulesSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/find_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; -import { FindRulesRequestParams } from '../../rules/types'; -import { findRulesSchema } from '../schemas/find_rules_schema'; import { transformValidateFindAlerts } from './validate'; -import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; export const findRulesRoute = (router: IRouter) => { router.get( { path: `${DETECTION_ENGINE_RULES_URL}/_find`, validate: { - query: buildRouteValidation(findRulesSchema), + query: buildRouteValidation( + findRulesSchema + ), }, options: { tags: ['access'], @@ -27,6 +33,10 @@ export const findRulesRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + const validationErrors = findRuleValidateTypeDependents(request.query); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } try { const { query } = request; @@ -45,6 +55,7 @@ export const findRulesRoute = (router: IRouter) => { sortField: query.sort_field, sortOrder: query.sort_order, filter: query.filter, + fields: query.fields, }); const ruleStatuses = await Promise.all( rules.data.map(async (rule) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index d7c6d317227fa..c2f396c874a7c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -66,7 +66,7 @@ describe('find_statuses', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith('"id" is not allowed'); + expect(result.badRequest).toHaveBeenCalledWith('Invalid value "undefined" supplied to "ids"'); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index a923c1bb49200..c02e28456111a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -4,28 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; -import { - FindRulesStatusesRequestParams, - RuleStatusResponse, - IRuleStatusAttributes, -} from '../../rules/types'; -import { - buildRouteValidation, - transformError, - convertToSnakeCase, - buildSiemResponse, -} from '../utils'; +import { RuleStatusResponse, IRuleStatusAttributes } from '../../rules/types'; +import { transformError, convertToSnakeCase, buildSiemResponse } from '../utils'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { + findRulesStatusesSchema, + FindRulesStatusesSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/find_rule_statuses_schema'; export const findRulesStatusesRoute = (router: IRouter) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, validate: { - body: buildRouteValidation(findRulesStatusesSchema), + body: buildRouteValidation( + findRulesStatusesSchema + ), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 31aad769d5a3d..c3f4695a20461 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -44,6 +44,7 @@ export const getPrepackagedRulesStatusRoute = (router: IRouter) => { sortField: 'enabled', sortOrder: 'desc', filter: 'alert.attributes.tags:"__internal_immutable:false"', + fields: undefined, }); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index a7d5e579b074d..0762fbc7dd6e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -250,14 +250,16 @@ describe('import_rules_route', () => { errors: [ { error: { - message: 'child "rule_id" fails because ["rule_id" is required]', + // TODO: Change the formatter to do better than output [object Object] + message: '[object Object],[object Object]', status_code: 400, }, rule_id: '(unknown id)', }, { error: { - message: 'child "rule_id" fails because ["rule_id" is required]', + // TODO: Change the formatter to do better than output [object Object] + message: '[object Object],[object Object]', status_code: 400, }, rule_id: '(unknown id)', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index cbe583a11d1c1..c8b61414608a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -8,8 +8,15 @@ import { chunk } from 'lodash/fp'; import { extname } from 'path'; import { - ImportRulesSchema, - importRulesSchema, + importRulesQuerySchema, + ImportRulesQuerySchemaDecoded, + importRulesPayloadSchema, + ImportRulesPayloadSchemaDecoded, + ImportRulesSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; +import { + ImportRulesSchema as ImportRulesResponseSchema, + importRulesSchema as importRulesResponseSchema, } from '../../../../../common/detection_engine/schemas/response/import_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; @@ -19,11 +26,9 @@ import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; -import { ImportRulesRequestParams } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; import { - buildRouteValidation, createBulkErrorObject, ImportRuleResponse, BulkError, @@ -32,14 +37,15 @@ import { transformError, buildSiemResponse, } from '../utils'; -import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; -import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { validate } from './validate'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { HapiReadableStream } from '../../rules/types'; +import { PartialFilter } from '../../types'; -type PromiseFromStreams = ImportRuleAlertRest | Error; +type PromiseFromStreams = ImportRulesSchemaDecoded | Error; const CHUNK_PARSED_OBJECT_SIZE = 10; @@ -48,8 +54,13 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP { path: `${DETECTION_ENGINE_RULES_URL}/_import`, validate: { - query: buildRouteValidation(importRulesQuerySchema), - body: buildRouteValidation(importRulesPayloadSchema), + query: buildRouteValidation( + importRulesQuerySchema + ), + body: buildRouteValidation< + typeof importRulesPayloadSchema, + ImportRulesPayloadSchemaDecoded + >(importRulesPayloadSchema), }, options: { tags: ['access:securitySolution'], @@ -74,7 +85,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); - const { filename } = request.body.file.hapi; + const { filename } = (request.body.file as HapiReadableStream).hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { return siemResponse.error({ @@ -94,7 +105,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP const objectLimit = config.maxRuleImportExportSize; const readStream = createRulesStreamFromNdJson(objectLimit); const parsedObjects = await createPromiseFromStreams([ - request.body.file, + request.body.file as HapiReadableStream, ...readStream, ]); const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueRules( @@ -128,13 +139,13 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP false_positives: falsePositives, from, immutable, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, meta, - filters, + filters: filtersRest, rule_id: ruleId, index, interval, @@ -151,13 +162,24 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP timeline_id: timelineId, timeline_title: timelineTitle, version, - exceptions_list, + exceptions_list: exceptionsList, } = parsedRule; try { + const query = + type !== 'machine_learning' && queryOrUndefined == null ? '' : queryOrUndefined; + + const language = + type !== 'machine_learning' && languageOrUndefined == null + ? 'kuery' + : languageOrUndefined; + + // TODO: Fix these either with an is conversion or by better typing them within io-ts + const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; + throwHttpError(await mlAuthz.validateRuleType(type)); - const rule = await readRules({ alertsClient, ruleId }); + const rule = await readRules({ alertsClient, ruleId, id: undefined }); if (rule == null) { await createRules({ alertsClient, @@ -190,7 +212,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP references, note, version, - exceptions_list, + exceptionsList, actions: [], // Actions are not imported nor exported at this time }); resolve({ rule_id: ruleId, status_code: 200 }); @@ -202,7 +224,6 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP enabled, falsePositives, from, - immutable, query, language, outputIndex, @@ -225,9 +246,10 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP references, note, version, - exceptions_list, + exceptionsList, anomalyThreshold, machineLearningJobId, + actions: undefined, }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null) { @@ -267,12 +289,12 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP return false; } }); - const importRules: ImportRulesSchema = { + const importRules: ImportRulesResponseSchema = { success: errorsResp.length === 0, success_count: successes.length, errors: errorsResp, }; - const [validated, errors] = validate(importRules, importRulesSchema); + const [validated, errors] = validate(importRules, importRulesResponseSchema); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { 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 24b2d5631b3a7..d5145e1dceae0 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 @@ -155,11 +155,18 @@ describe('patch_rules_bulk', () => { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, body: [{ ...typicalPayload(), rule_id: undefined }], }); - const result = server.validate(request); + const response = await server.inject(request, context); - expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because ["value" must contain at least one of [id, rule_id]]' - ); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + error: { + message: 'id or rule_id should have been defined', + status_code: 404, + }, + rule_id: '(unknown id)', + }, + ]); }); test('allows query rule type', async () => { @@ -182,7 +189,7 @@ describe('patch_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' + 'Invalid value "unknown_type" supplied to "type"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index d1347786ddc1b..16f491547a9e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -4,28 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; +import { + patchRulesBulkSchema, + PatchRulesBulkSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/patch_rules_bulk_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; 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 { PatchRuleAlertParamsRest } from '../../rules/types'; -import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; +import { transformBulkError, buildSiemResponse } from '../utils'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; -import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { readRules } from '../../rules/read_rules'; +import { PartialFilter } from '../../types'; export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.patch( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, validate: { - body: buildRouteValidation(patchRulesBulkSchema), + body: buildRouteValidation( + patchRulesBulkSchema + ), }, options: { tags: ['access:securitySolution'], @@ -46,7 +53,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const rules = await Promise.all( request.body.map(async (payloadRule) => { const { - actions, + actions: actionsRest, description, enabled, false_positives: falsePositives, @@ -58,7 +65,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => timeline_id: timelineId, timeline_title: timelineTitle, meta, - filters, + filters: filtersRest, rule_id: ruleId, id, index, @@ -77,8 +84,13 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => version, anomaly_threshold: anomalyThreshold, machine_learning_job_id: machineLearningJobId, + exceptions_list: exceptionsList, } = payloadRule; const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + // 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[]; + try { if (type) { // reject an unauthorized "promotion" to ML @@ -123,6 +135,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => anomalyThreshold, machineLearningJobId, actions, + exceptionsList, }); if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ 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 9ae7e83ef7989..09230d45ee785 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 @@ -167,11 +167,11 @@ describe('patch_rules', () => { path: DETECTION_ENGINE_RULES_URL, body: { ...typicalPayload(), rule_id: undefined }, }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith( - '"value" must contain at least one of [id, rule_id]' - ); + const response = await server.inject(request, context); + expect(response.body).toEqual({ + message: ['either "id" or "rule_id" must be set'], + status_code: 400, + }); }); test('allows query rule type', async () => { @@ -194,7 +194,7 @@ describe('patch_rules', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' + 'Invalid value "unknown_type" supplied to "type"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 8f0d222ebaa64..385eec0fe1180 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -4,35 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; +import { patchRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/patch_rules_type_dependents'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { + PatchRulesSchemaDecoded, + patchRulesSchema, +} from '../../../../../common/detection_engine/schemas/request/patch_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 { patchRules } from '../../rules/patch_rules'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { patchRulesSchema } from '../schemas/patch_rules_schema'; -import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { readRules } from '../../rules/read_rules'; +import { PartialFilter } from '../../types'; export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.patch( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation(patchRulesSchema), + body: buildRouteValidation( + patchRulesSchema + ), }, options: { tags: ['access:securitySolution'], }, }, async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + const validationErrors = patchRuleValidateTypeDependents(request.body); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } const { - actions, + actions: actionsRest, description, enabled, false_positives: falsePositives, @@ -44,7 +57,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { timeline_id: timelineId, timeline_title: timelineTitle, meta, - filters, + filters: filtersRest, rule_id: ruleId, id, index, @@ -63,10 +76,13 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { version, anomaly_threshold: anomalyThreshold, machine_learning_job_id: machineLearningJobId, + exceptions_list: exceptionsList, } = request.body; - const siemResponse = buildSiemResponse(response); - try { + // 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; @@ -119,6 +135,7 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { anomalyThreshold, machineLearningJobId, actions, + exceptionsList, }); if (rule != null && rule.enabled != null && rule.name != null) { const ruleActions = await updateRulesNotifications({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index c9b218a3f0898..4a568643ccc2e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -4,14 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; +import { + queryRulesSchema, + QueryRulesSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/query_rules_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; -import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { ReadRuleRequestParams } from '../../rules/types'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; @@ -20,15 +24,22 @@ export const readRulesRoute = (router: IRouter) => { { path: DETECTION_ENGINE_RULES_URL, validate: { - query: buildRouteValidation(queryRulesSchema), + query: buildRouteValidation( + queryRulesSchema + ), }, options: { tags: ['access:securitySolution'], }, }, async (context, request, response) => { - const { id, rule_id: ruleId } = request.query; const siemResponse = buildSiemResponse(response); + const validationErrors = queryRuleValidateTypeDependents(request.query); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } + + const { id, rule_id: ruleId } = request.query; const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; 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 74b135bcb4d8c..2f331938e3ca8 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 @@ -126,16 +126,18 @@ describe('update_rules_bulk', () => { describe('request validation', () => { test('rejects payloads with no ID', async () => { - const request = requestMock.create({ + const noIdRequest = requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, body: [{ ...typicalPayload(), rule_id: undefined }], }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because ["value" must contain at least one of [id, rule_id]]' - ); + const response = await server.inject(noIdRequest, context); + expect(response.body).toEqual([ + { + error: { message: 'either "id" or "rule_id" must be set', status_code: 400 }, + rule_id: '(unknown id)', + }, + ]); }); test('allows query rule type', async () => { @@ -158,7 +160,7 @@ describe('update_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' + 'Invalid value "unknown_type" supplied to "type"' ); }); }); 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 ec2b0168c18bf..3ca4a28dd93ee 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 @@ -4,27 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ +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 { 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 { UpdateRuleAlertParamsRest } from '../../rules/types'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; -import { buildRouteValidation, transformBulkError, buildSiemResponse } from '../utils'; -import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; +import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; 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'], @@ -46,21 +54,21 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const rules = await Promise.all( request.body.map(async (payloadRule) => { const { - actions, + actions: actionsRest, anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, timeline_title: timelineTitle, meta, - filters, + filters: filtersRest, rule_id: ruleId, id, index, @@ -77,11 +85,32 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => references, note, version, - exceptions_list, + exceptions_list: exceptionsList, } = payloadRule; const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { + const validationErrors = updateRuleValidateTypeDependents(payloadRule); + if (validationErrors.length) { + return createBulkErrorObject({ + ruleId, + statusCode: 400, + message: validationErrors.join(), + }); + } + + const query = + type !== 'machine_learning' && queryOrUndefined == null ? '' : queryOrUndefined; + + const language = + type !== 'machine_learning' && 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 rule = await updateRules({ @@ -116,7 +145,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => references, note, version, - exceptions_list, + exceptionsList, actions, }); if (rule != null) { 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 25556d8d10fd4..f8b7636080b1b 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 @@ -138,13 +138,16 @@ describe('update_rules', () => { const noIdRequest = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: { ...typicalPayload(), rule_id: undefined }, + body: { + ...typicalPayload(), + rule_id: undefined, + }, + }); + const response = await server.inject(noIdRequest, context); + expect(response.body).toEqual({ + message: ['either "id" or "rule_id" must be set'], + status_code: 400, }); - const result = await server.validate(noIdRequest); - - expect(result.badRequest).toHaveBeenCalledWith( - '"value" must contain at least one of [id, rule_id]' - ); }); test('allows query rule type', async () => { @@ -167,7 +170,7 @@ describe('update_rules', () => { const result = await server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' + 'Invalid value "unknown type" supplied to "type"' ); }); }); 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 2680db8abdd4d..f2b47f195ca5c 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,48 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ +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 { 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 { UpdateRuleAlertParamsRest } from '../../rules/types'; -import { updateRulesSchema } from '../schemas/update_rules_schema'; -import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { 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'], }, }, async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + const validationErrors = updateRuleValidateTypeDependents(request.body); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } + const { - actions, + actions: actionsRest, anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, timeline_title: timelineTitle, meta, - filters, + filters: filtersRest, rule_id: ruleId, id, index, @@ -62,11 +76,21 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { references, note, version, - exceptions_list, + exceptions_list: exceptionsList, } = request.body; - const siemResponse = buildSiemResponse(response); - try { + const query = + type !== 'machine_learning' && queryOrUndefined == null ? '' : queryOrUndefined; + + const language = + type !== 'machine_learning' && 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(); @@ -112,7 +136,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { references, note, version, - exceptions_list, + exceptionsList, actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it }); 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 df158d23c0e24..3b2750bbbf664 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 @@ -19,7 +19,7 @@ import { } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../types'; +import { RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; @@ -28,8 +28,10 @@ import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; +import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; -type PromiseFromStreams = ImportRuleAlertRest | Error; +type PromiseFromStreams = ImportRulesSchemaDecoded | Error; describe('utils', () => { beforeAll(() => { @@ -485,7 +487,7 @@ describe('utils', () => { { rule_id: 'value3' }, {}, {}, - ] as RuleAlertParamsRest[], + ] as CreateRulesBulkSchemaDecoded, 'rule_id' ); const expected = ['value2', 'value3']; @@ -499,7 +501,7 @@ describe('utils', () => { { rule_id: 'value3' }, {}, {}, - ] as RuleAlertParamsRest[], + ] as CreateRulesBulkSchemaDecoded, '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 5329ff04435ca..3ed45bd8367fc 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 @@ -8,6 +8,8 @@ import { pickBy, countBy } from 'lodash/fp'; import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import uuid from 'uuid'; +import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; +import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { @@ -19,7 +21,7 @@ import { isRuleStatusFindTypes, isRuleStatusSavedObjectType, } from '../../rules/types'; -import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; +import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError, @@ -29,10 +31,9 @@ import { OutputError, } from '../utils'; import { hasListsFeature } from '../../feature_flags'; -// import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; import { RuleActions } from '../../rule_actions/types'; -type PromiseFromStreams = ImportRuleAlertRest | Error; +type PromiseFromStreams = ImportRulesSchemaDecoded | Error; export const getIdError = ({ id, @@ -148,7 +149,7 @@ export const transformAlertToRule = ( last_failure_message: ruleStatus?.attributes.lastFailureMessage, last_success_message: ruleStatus?.attributes.lastSuccessMessage, // TODO: (LIST-FEATURE) Remove hasListsFeature() check once we have lists available for a release - exceptions_list: hasListsFeature() ? alert.params.exceptions_list : null, + exceptions_list: hasListsFeature() ? alert.params.exceptionsList : null, }); }; @@ -243,7 +244,10 @@ export const transformOrImportError = ( } }; -export const getDuplicates = (ruleDefinitions: RuleAlertParamsRest[], by: 'rule_id'): string[] => { +export const getDuplicates = ( + ruleDefinitions: CreateRulesBulkSchemaDecoded, + by: 'rule_id' +): string[] => { const mappedDuplicates = countBy( by, ruleDefinitions.filter((r) => r[by] != null) @@ -265,21 +269,17 @@ export const getTupleDuplicateErrorsAndUniqueRules = ( acc.rulesAcc.set(uuid.v4(), parsedRule); } else { const { rule_id: ruleId } = parsedRule; - if (ruleId != null) { - if (acc.rulesAcc.has(ruleId) && !isOverwrite) { - acc.errors.set( - uuid.v4(), - createBulkErrorObject({ - ruleId, - statusCode: 400, - message: `More than one rule with rule-id: "${ruleId}" found`, - }) - ); - } - acc.rulesAcc.set(ruleId, parsedRule); - } else { - acc.rulesAcc.set(uuid.v4(), parsedRule); + if (acc.rulesAcc.has(ruleId) && !isOverwrite) { + acc.errors.set( + uuid.v4(), + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: `More than one rule with rule-id: "${ruleId}" found`, + }) + ); } + acc.rulesAcc.set(ruleId, parsedRule); } return acc; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts deleted file mode 100644 index 66356a1d65352..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ /dev/null @@ -1,1658 +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 { AlertAction } from '../../../../../../alerts/common'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { ThreatParams, PrepackagedRules } from '../../types'; -import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('add prepackaged rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('empty objects do not validate', () => { - expect(addPrepackagedRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[rule_id] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval, version] does validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, version] does validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does not validate because output_index is not allowed', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - version: 1, - }).error.message - ).toEqual('"output_index" is not allowed'); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version] does validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You can send in an empty array to threat', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [], - version: 1, - }).error - ).toBeFalsy(); - }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version, s] does validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - version: 1, - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as valid', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('defaults references to an array', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).value.references - ).toEqual([]); - }); - - test('defaults immutable to true', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).value.immutable - ).toEqual(true); - }); - - test('immutable cannot be set in a pre-packaged rule', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - immutable: true, - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - version: 1, - }).error.message - ).toEqual('child "immutable" fails because ["immutable" is not allowed]'); - }); - - test('defaults enabled to false', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).value.enabled - ).toEqual(false); - }); - - test('rule_id is required', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).error.message - ).toEqual('child "rule_id" fails because ["rule_id" is required]'); - }); - - test('references cannot be numbers', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { references: number[] } - >({ - 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: [5], - version: 1, - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('indexes cannot be numbers', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { index: number[] } - >({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - version: 1, - }).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('defaults interval to 5 min', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - version: 1, - }).value.interval - ).toEqual('5m'); - }); - - test('defaults max signals to 100', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - version: 1, - }).value.max_signals - ).toEqual(100); - }); - - test('saved_id is required when type is saved_query and will not validate without out', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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: 'saved_query', - version: 1, - }).error.message - ).toEqual('child "saved_id" fails because ["saved_id" is required]'); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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: 'saved_query', - saved_id: 'some id', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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: 'saved_query', - saved_id: 'some id', - filters: [], - version: 1, - }).error - ).toBeFalsy(); - }); - - test('filters cannot be a string', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial & { filters: string }> - >({ - 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: 'saved_query', - saved_id: 'some id', - filters: 'some string', - version: 1, - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('language validates with kuery', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'lucene', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - version: 1, - }).error.message - ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); - }); - - test('max_signals cannot be negative', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - version: 1, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals cannot be zero', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - version: 1, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals can be 1', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You can optionally send in an array of tags', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: ['tag_1', 'tag_2'], - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of tags that are numbers', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { tags: number[] } - >({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - version: 1, - }).error.message - ).toEqual( - 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - version: 1, - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - version: 1, - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - version: 1, - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' - ); - }); - - test('You can optionally send in an array of false positives', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - false_positives: ['false_1', 'false_2'], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { false_positives: number[] } - >({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - false_positives: [5, 4], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot set the risk_score to 101', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 101, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); - }); - - test('You cannot set the risk_score to -1', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: -1, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); - }); - - test('You can set the risk_score to 0', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 0, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set the risk_score to 100', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 100, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set meta to any object you want', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot create meta as a string', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial & { meta: string }> - >({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: 'should not work', - version: 1, - }).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('You can omit the query string when filters are present', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - version: 1, - }).error - ).toBeFalsy(); - }); - - test('validates with timeline_id and timeline_title', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - version: 1, - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }).error - ).toBeFalsy(); - }); - - test('You cannot omit timeline_title when timeline_id is present', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - version: 1, - timeline_id: 'timeline-id', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); - }); - - test('You cannot have a null value for timeline_title when timeline_id is present', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - version: 1, - timeline_id: 'timeline-id', - timeline_title: null, - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); - }); - - test('You cannot have empty string for timeline_title when timeline_id is present', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - version: 1, - timeline_id: 'timeline-id', - timeline_title: '', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title with an empty timeline_id', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - version: 1, - timeline_id: '', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title without timeline_id', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - version: 1, - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); - }); - - test('The default for "from" will be "now-6m"', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.from - ).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - index: ['auditbeat-*'], - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.to - ).toEqual('now'); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - index: ['auditbeat-*'], - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' - ); - }); - - test('The default for "actions" will be an empty array', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - index: ['auditbeat-*'], - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.actions - ).toEqual([]); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - actions: Array>; - } - >({ - actions: [ - { - id: 'id', - action_type_id: 'actionTypeId', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - actions: Array>; - } - >({ - actions: [ - { - group: 'group', - action_type_id: 'action_type_id', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - actions: Array>; - } - >({ - actions: [ - { - group: 'group', - id: 'id', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - actions: Array>; - } - >({ - actions: [ - { - group: 'group', - id: 'id', - action_type_id: 'action_type_id', - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are including "actionTypeId', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial> & { - actions: AlertAction[]; - } - >({ - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('The default for "throttle" will be null', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - index: ['auditbeat-*'], - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.throttle - ).toEqual(null); - }); - - describe('note', () => { - test('You can set note to any string you want', () => { - expect( - addPrepackagedRulesSchema.validate>({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - note: '# test header', - version: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot create note as anything other than a string', () => { - expect( - addPrepackagedRulesSchema.validate< - Partial & { note: object }> - >({ - 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', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - note: { - somethingMadeUp: { somethingElse: true }, - }, - version: 1, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - }); - - // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema - // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the - // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, - // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('exceptions_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - version: 1, - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - expect( - addPrepackagedRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [], - version: 1, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - expect( - addPrepackagedRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [{ invalid_value: 'invalid value' }], - version: 1, - }).error.message - ).toEqual( - 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' - ); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - expect( - addPrepackagedRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - version: 1, - }).value.exceptions_list - ).toEqual([]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts deleted file mode 100644 index f1bffdaf83522..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ /dev/null @@ -1,115 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { - actions, - enabled, - description, - false_positives, - filters, - from, - immutable, - index, - rule_id, - interval, - query, - language, - saved_id, - timeline_id, - timeline_title, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - throttle, - references, - note, - version, - lists, - anomaly_threshold, - machine_learning_job_id, -} from './schemas'; -/* eslint-enable @typescript-eslint/camelcase */ - -import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; -import { hasListsFeature } from '../../feature_flags'; - -/** - * Big differences between this schema and the createRulesSchema - * - rule_id is required here - * - output_index is not allowed (and instead the space index must be used) - * - immutable is forbidden but defaults to true instead of to false and it can only ever be true - * - enabled defaults to false instead of true - * - version is a required field that must exist - * - index is a required field that must exist if type !== machine_learning - */ -export const addPrepackagedRulesSchema = Joi.object({ - actions: actions.default([]), - anomaly_threshold: anomaly_threshold.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - description: description.required(), - enabled: enabled.default(false), - false_positives: false_positives.default([]), - filters, - from: from.default('now-6m'), - rule_id: rule_id.required(), - immutable: immutable.forbidden().default(true).valid(true), - index: index.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: Joi.required(), - }), - interval: interval.default('5m'), - query: query.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: query.allow('').default(''), - }), - language: language.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: language.default('kuery'), - }), - machine_learning_job_id: machine_learning_job_id.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - saved_id: saved_id.when('type', { - is: 'saved_query', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - timeline_id, - timeline_title, - meta, - risk_score: risk_score.required(), - max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), - name: name.required(), - severity: severity.required(), - tags: tags.default([]), - to: to.default('now'), - type: type.required(), - threat: threat.default([]), - throttle: throttle.default(null), - references: references.default([]), - note: note.allow(''), - version: version.required(), - - // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts deleted file mode 100644 index 0bf59759a6db6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts +++ /dev/null @@ -1,260 +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 { createRulesBulkSchema } from './create_rules_bulk_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -// only the basics of testing are here. -// see: create_rules_schema.test.ts for the bulk of the validation tests -// this just wraps createRulesSchema in an array -describe('create_rules_bulk_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('can take an empty array and validate it', () => { - expect( - createRulesBulkSchema.validate>>([]).error - ).toBeFalsy(); - }); - - test('made up values do not validate', () => { - expect( - createRulesBulkSchema.validate<[{ madeUp: string }]>([ - { - madeUp: 'hi', - }, - ]).error - ).toBeTruthy(); - }); - - test('single array of [id] does validate', () => { - expect( - createRulesBulkSchema.validate>>([ - { - 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', - }, - ]).error - ).toBeFalsy(); - }); - - test('two values of [id] does validate', () => { - expect( - createRulesBulkSchema.validate>>([ - { - 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', - }, - { - rule_id: 'rule-2', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }, - ]).error - ).toBeFalsy(); - }); - - test('The default for "from" will be "now-6m"', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }, - ]).value[0].from - ).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }, - ]).value[0].to - ).toEqual('now'); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }, - ]).error.message - ).toEqual( - '"value" at position 0 fails because [child "severity" fails because ["severity" must be one of [low, medium, high, critical]]]' - ); - }); - - test('You can set "note" to a string', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: '# test markdown', - version: 1, - }, - ]).error - ).toBeFalsy(); - }); - - test('You can set "note" to an empty string', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: '', - version: 1, - }, - ]).error - ).toBeFalsy(); - }); - - test('You cannot set "note" to anything other than string', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: { - something: 'some object', - }, - version: 1, - }, - ]).error.message - ).toEqual( - '"value" at position 0 fails because [child "note" fails because ["note" must be a string]]' - ); - }); - - test('The default for "actions" will be an empty array', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }, - ]).value[0].actions - ).toEqual([]); - }); - - test('The default for "throttle" will be null', () => { - expect( - createRulesBulkSchema.validate>([ - { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }, - ]).value[0].throttle - ).toEqual(null); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts deleted file mode 100644 index bcc4475f2d9f0..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -import { createRulesSchema } from './create_rules_schema'; - -export const createRulesBulkSchema = Joi.array().items(createRulesSchema); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts deleted file mode 100644 index 013db2020a146..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ /dev/null @@ -1,1623 +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 { AlertAction } from '../../../../../../alerts/common'; -import { createRulesSchema } from './create_rules_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { ThreatParams, RuleAlertParamsRest } from '../../types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('create rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('empty objects do not validate', () => { - expect(createRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - createRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[rule_id] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - expect( - createRulesSchema.validate>({ - 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', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - expect( - createRulesSchema.validate>({ - 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', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('You can send in an empty array to threat', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as valid', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('defaults references to an array', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).value.references - ).toEqual([]); - }); - - test('references cannot be numbers', () => { - expect( - createRulesSchema.validate< - Partial> & { references: number[] } - >({ - 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: [5], - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('indexes cannot be numbers', () => { - expect( - createRulesSchema.validate> & { index: number[] }>( - { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - } - ).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('defaults interval to 5 min', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - }).value.interval - ).toEqual('5m'); - }); - - test('defaults max signals to 100', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).value.max_signals - ).toEqual(100); - }); - - test('saved_id is required when type is saved_query and will not validate without out', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - }).error.message - ).toEqual('child "saved_id" fails because ["saved_id" is required]'); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); - }); - - test('filters cannot be a string', () => { - expect( - createRulesSchema.validate< - Partial & { filters: string }> - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: 'some string', - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('language validates with kuery', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'lucene', - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - createRulesSchema.validate< - Partial & { language: string }> - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - }).error.message - ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); - }); - - test('max_signals cannot be negative', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals cannot be zero', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals can be 1', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can optionally send in an array of tags', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: ['tag_1', 'tag_2'], - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of tags that are numbers', () => { - expect( - createRulesSchema.validate> & { tags: number[] }>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - }).error.message - ).toEqual( - 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - expect( - createRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - expect( - createRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - expect( - createRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' - ); - }); - - test('You can optionally send in an array of false positives', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: ['false_1', 'false_2'], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - expect( - createRulesSchema.validate< - Partial> & { false_positives: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: [5, 4], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual( - 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot set the immutable when trying to create a rule', () => { - expect( - createRulesSchema.validate< - Partial> & { immutable: number } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: 5, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('"immutable" is not allowed'); - }); - - test('You cannot set the risk_score to 101', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 101, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); - }); - - test('You cannot set the risk_score to -1', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: -1, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); - }); - - test('You can set the risk_score to 0', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 0, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set the risk_score to 100', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 100, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set meta to any object you want', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }).error - ).toBeFalsy(); - }); - - test('You cannot create meta as a string', () => { - expect( - createRulesSchema.validate & { meta: string }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: 'should not work', - }).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('You can omit the query string when filters are present', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('validates with timeline_id and timeline_title', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }).error - ).toBeFalsy(); - }); - - test('You cannot omit timeline_title when timeline_id is present', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); - }); - - test('You cannot have a null value for timeline_title when timeline_id is present', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - timeline_title: null, - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); - }); - - test('You cannot have empty string for timeline_title when timeline_id is present', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - timeline_title: '', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title with an empty timeline_id', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: '', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title without timeline_id', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); - }); - - test('The default for "from" will be "now-6m"', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.from - ).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.to - ).toEqual('now'); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' - ); - }); - - test('The default for "actions" will be an empty array', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.actions - ).toEqual([]); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - expect( - createRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - expect( - createRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - expect( - createRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - expect( - createRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are including "actionTypeId"', () => { - expect( - createRulesSchema.validate< - Partial< - Omit & { - actions: AlertAction[]; - } - > - >({ - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('The default for "throttle" will be null', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.throttle - ).toEqual(null); - }); - - describe('note', () => { - test('You can set note to a string', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: '# documentation markdown here', - }).error - ).toBeFalsy(); - }); - - test('You can set note to an emtpy string', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: '', - }).error - ).toBeFalsy(); - }); - - test('You cannot create note as an object', () => { - expect( - createRulesSchema.validate & { note: object }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: { - somethingHere: 'something else', - }, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }).error - ).toBeFalsy(); - }); - - // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema - // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the - // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, - // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('exceptions_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - expect( - createRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [{ invalid_value: 'invalid value' }], - }).error.message - ).toEqual( - 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' - ); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - expect( - createRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }).value.exceptions_list - ).toEqual([]); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_schema.ts deleted file mode 100644 index dec8b5ccbc790..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ /dev/null @@ -1,102 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { - actions, - anomaly_threshold, - enabled, - description, - false_positives, - filters, - from, - index, - rule_id, - interval, - query, - language, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - throttle, - references, - note, - version, - lists, - machine_learning_job_id, -} from './schemas'; -/* eslint-enable @typescript-eslint/camelcase */ - -import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; -import { hasListsFeature } from '../../feature_flags'; - -export const createRulesSchema = Joi.object({ - actions: actions.default([]), - anomaly_threshold: anomaly_threshold.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - description: description.required(), - enabled: enabled.default(true), - false_positives: false_positives.default([]), - filters, - from: from.default('now-6m'), - rule_id, - index, - interval: interval.default('5m'), - query: query.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: query.allow('').default(''), - }), - language: language.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: language.default('kuery'), - }), - output_index, - saved_id: saved_id.when('type', { - is: 'saved_query', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - timeline_id, - timeline_title, - meta, - machine_learning_job_id: machine_learning_job_id.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - risk_score: risk_score.required(), - max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), - name: name.required(), - severity: severity.required(), - tags: tags.default([]), - to: to.default('now'), - type: type.required(), - threat: threat.default([]), - throttle: throttle.default(null), - references: references.default([]), - note: note.allow(''), - version: version.default(1), - - // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts deleted file mode 100644 index 0e71237f75232..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts +++ /dev/null @@ -1,113 +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 { exportRulesSchema, exportRulesQuerySchema } from './export_rules_schema'; -import { ExportRulesRequestParams } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('create rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - describe('exportRulesSchema', () => { - test('null value or absent values validate', () => { - expect(exportRulesSchema.validate(null).error).toBeFalsy(); - }); - - test('empty object does not validate', () => { - expect( - exportRulesSchema.validate>({}).error - ).toBeTruthy(); - }); - - test('empty object array does validate', () => { - expect( - exportRulesSchema.validate>({ objects: [] }).error - ).toBeTruthy(); - }); - - test('array with rule_id validates', () => { - expect( - exportRulesSchema.validate>({ - objects: [{ rule_id: 'test-1' }], - }).error - ).toBeFalsy(); - }); - - test('array with id does not validate as we do not allow that on purpose since we export rule_id', () => { - expect( - exportRulesSchema.validate>({ - objects: [{ id: 'test-1' }], - }).error.message - ).toEqual( - 'child "objects" fails because ["objects" at position 0 fails because ["id" is not allowed]]' - ); - }); - }); - - describe('exportRulesQuerySchema', () => { - test('default value for file_name is export.ndjson', () => { - expect( - exportRulesQuerySchema.validate>({}).value - .file_name - ).toEqual('export.ndjson'); - }); - - test('default value for exclude_export_details is false', () => { - expect( - exportRulesQuerySchema.validate>({}).value - .exclude_export_details - ).toEqual(false); - }); - - test('file_name validates', () => { - expect( - exportRulesQuerySchema.validate>({ - file_name: 'test.ndjson', - }).error - ).toBeFalsy(); - }); - - test('file_name does not validate with a number', () => { - expect( - exportRulesQuerySchema.validate< - Partial & { file_name: number }> - >({ - file_name: 5, - }).error.message - ).toEqual('child "file_name" fails because ["file_name" must be a string]'); - }); - - test('exclude_export_details validates with a boolean true', () => { - expect( - exportRulesQuerySchema.validate>({ - exclude_export_details: true, - }).error - ).toBeFalsy(); - }); - - test('exclude_export_details does not validate with a weird string', () => { - expect( - exportRulesQuerySchema.validate< - Partial< - Omit & { - exclude_export_details: string; - } - > - >({ - exclude_export_details: 'blah', - }).error.message - ).toEqual( - 'child "exclude_export_details" fails because ["exclude_export_details" must be a boolean]' - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/export_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/export_rules_schema.ts deleted file mode 100644 index a14d81604d9f8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/export_rules_schema.ts +++ /dev/null @@ -1,22 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { objects, exclude_export_details, file_name } from './schemas'; -/* eslint-disable @typescript-eslint/camelcase */ - -export const exportRulesSchema = Joi.object({ - objects, -}) - .min(1) - .allow(null); - -export const exportRulesQuerySchema = Joi.object({ - file_name: file_name.default('export.ndjson'), - exclude_export_details: exclude_export_details.default(false), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts deleted file mode 100644 index ffbfd193873a8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts +++ /dev/null @@ -1,147 +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 { findRulesSchema } from './find_rules_schema'; -import { FindParamsRest } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('find rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('empty objects do validate', () => { - expect(findRulesSchema.validate>({}).error).toBeFalsy(); - }); - - test('all values validate', () => { - expect( - findRulesSchema.validate>({ - per_page: 5, - page: 1, - sort_field: 'some field', - fields: ['field 1', 'field 2'], - filter: 'some filter', - sort_order: 'asc', - }).error - ).toBeFalsy(); - }); - - test('made up parameters do not validate', () => { - expect( - findRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('per_page validates', () => { - expect( - findRulesSchema.validate>({ per_page: 5 }).error - ).toBeFalsy(); - }); - - test('page validates', () => { - expect( - findRulesSchema.validate>({ page: 5 }).error - ).toBeFalsy(); - }); - - test('sort_field validates', () => { - expect( - findRulesSchema.validate>({ sort_field: 'some value' }).error - ).toBeFalsy(); - }); - - test('fields validates with a string', () => { - expect( - findRulesSchema.validate>({ fields: ['some value'] }).error - ).toBeFalsy(); - }); - - test('fields validates with multiple strings', () => { - expect( - findRulesSchema.validate>({ - fields: ['some value 1', 'some value 2'], - }).error - ).toBeFalsy(); - }); - - test('fields does not validate with a number', () => { - expect( - findRulesSchema.validate> & { fields: number[] }>({ - fields: [5], - }).error.message - ).toEqual( - 'child "fields" fails because ["fields" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('per page has a default of 20', () => { - expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); - }); - - test('page has a default of 1', () => { - expect(findRulesSchema.validate>({}).value.page).toEqual(1); - }); - - test('filter works with a string', () => { - expect( - findRulesSchema.validate>({ - filter: 'some value 1', - }).error - ).toBeFalsy(); - }); - - test('filter does not work with a number', () => { - expect( - findRulesSchema.validate> & { filter: number }>({ - filter: 5, - }).error.message - ).toEqual('child "filter" fails because ["filter" must be a string]'); - }); - - test('sort_order requires sort_field to work', () => { - expect( - findRulesSchema.validate>({ - sort_order: 'asc', - }).error.message - ).toEqual('child "sort_field" fails because ["sort_field" is required]'); - }); - - test('sort_order and sort_field validate together', () => { - expect( - findRulesSchema.validate>({ - sort_order: 'asc', - sort_field: 'some field', - }).error - ).toBeFalsy(); - }); - - test('sort_order validates with desc and sort_field', () => { - expect( - findRulesSchema.validate>({ - sort_order: 'desc', - sort_field: 'some field', - }).error - ).toBeFalsy(); - }); - - test('sort_order does not validate with a string other than asc and desc', () => { - expect( - findRulesSchema.validate< - Partial> & { sort_order: string } - >({ - sort_order: 'some other string', - sort_field: 'some field', - }).error.message - ).toEqual('child "sort_order" fails because ["sort_order" must be one of [asc, desc]]'); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_schema.ts deleted file mode 100644 index 3cc5b9ca44530..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_schema.ts +++ /dev/null @@ -1,24 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { queryFilter, fields, per_page, page, sort_field, sort_order } from './schemas'; -/* eslint-enable @typescript-eslint/camelcase */ - -export const findRulesSchema = Joi.object({ - fields, - filter: queryFilter, - per_page, - page, - sort_field: Joi.when(Joi.ref('sort_order'), { - is: Joi.exist(), - then: sort_field.required(), - otherwise: sort_field.optional(), - }), - sort_order, -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts deleted file mode 100644 index 5b8661e69e206..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/find_rules_statuses_schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -export const findRulesStatusesSchema = Joi.object({ - ids: Joi.array().items(Joi.string()), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts deleted file mode 100644 index cb03c4781cb6c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ /dev/null @@ -1,1841 +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 { AlertAction } from '../../../../../../alerts/common'; -import { - importRulesSchema, - importRulesQuerySchema, - importRulesPayloadSchema, -} from './import_rules_schema'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { ThreatParams, ImportRuleAlertRest } from '../../types'; -import { ImportRulesRequestParams } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('import rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - describe('importRulesSchema', () => { - test('empty objects do not validate', () => { - expect(importRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - importRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[rule_id] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - expect( - importRulesSchema.validate>({ - 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', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - expect( - importRulesSchema.validate>({ - 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', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('You can send in an empty array to threat', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as valid', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('defaults references to an array', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).value.references - ).toEqual([]); - }); - - test('references cannot be numbers', () => { - expect( - importRulesSchema.validate< - Partial> & { references: number[] } - >({ - 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: [5], - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('indexes cannot be numbers', () => { - expect( - importRulesSchema.validate< - Partial> & { index: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('defaults interval to 5 min', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - }).value.interval - ).toEqual('5m'); - }); - - test('defaults max signals to 100', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).value.max_signals - ).toEqual(100); - }); - - test('saved_id is required when type is saved_query and will not validate without out', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - }).error.message - ).toEqual('child "saved_id" fails because ["saved_id" is required]'); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); - }); - - test('filters cannot be a string', () => { - expect( - importRulesSchema.validate< - Partial & { filters: string }> - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: 'some string', - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('language validates with kuery', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'lucene', - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - }).error.message - ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); - }); - - test('max_signals cannot be negative', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals cannot be zero', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals can be 1', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can optionally send in an array of tags', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: ['tag_1', 'tag_2'], - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of tags that are numbers', () => { - expect( - importRulesSchema.validate> & { tags: number[] }>( - { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - } - ).error.message - ).toEqual( - 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - expect( - importRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - expect( - importRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - expect( - importRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' - ); - }); - - test('You can optionally send in an array of false positives', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: ['false_1', 'false_2'], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - expect( - importRulesSchema.validate< - Partial> & { false_positives: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: [5, 4], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual( - 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You can optionally set the immutable to be false', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannnot set immutable to be true', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "immutable" fails because ["immutable" must be one of [false]]'); - }); - - test('You cannot set the immutable to be a number', () => { - expect( - importRulesSchema.validate< - Partial> & { immutable: number } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: 5, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "immutable" fails because ["immutable" must be a boolean]'); - }); - - test('You cannot set the risk_score to 101', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 101, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); - }); - - test('You cannot set the risk_score to -1', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: -1, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); - }); - - test('You can set the risk_score to 0', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 0, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set the risk_score to 100', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 100, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set meta to any object you want', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }).error - ).toBeFalsy(); - }); - - test('You cannot create meta as a string', () => { - expect( - importRulesSchema.validate & { meta: string }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: 'should not work', - }).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('You can omit the query string when filters are present', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('validates with timeline_id and timeline_title', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }).error - ).toBeFalsy(); - }); - - test('You cannot omit timeline_title when timeline_id is present', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); - }); - - test('You cannot have a null value for timeline_title when timeline_id is present', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - timeline_title: null, - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); - }); - - test('You cannot have empty string for timeline_title when timeline_id is present', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - timeline_title: '', - }).error.message - ).toEqual( - 'child "timeline_title" fails because ["timeline_title" is not allowed to be empty]' - ); - }); - - test('You cannot have timeline_title with an empty timeline_id', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: '', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title without timeline_id', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); - }); - - test('rule_id is required and you cannot get by with just id', () => { - expect( - importRulesSchema.validate>({ - id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error.message - ).toEqual('child "rule_id" fails because ["rule_id" is required]'); - }); - - test('it validates with created_at, updated_at, created_by, updated_by values', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - created_at: '2020-01-09T06:15:24.749Z', - updated_at: '2020-01-09T06:15:24.749Z', - created_by: 'Braden Hassanabad', - updated_by: 'Evan Hassanabad', - }).error - ).toBeFalsy(); - }); - - test('it does not validate with epoch strings for created_at', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - created_at: '1578550728650', - updated_at: '2020-01-09T06:15:24.749Z', - created_by: 'Braden Hassanabad', - updated_by: 'Evan Hassanabad', - }).error.message - ).toEqual('child "created_at" fails because ["created_at" must be a valid ISO 8601 date]'); - }); - - test('it does not validate with epoch strings for updated_at', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - created_at: '2020-01-09T06:15:24.749Z', - updated_at: '1578550728650', - created_by: 'Braden Hassanabad', - updated_by: 'Evan Hassanabad', - }).error.message - ).toEqual('child "updated_at" fails because ["updated_at" must be a valid ISO 8601 date]'); - }); - }); - - describe('importRulesQuerySchema', () => { - test('overwrite gets a default value of false', () => { - expect( - importRulesQuerySchema.validate>({}).value - .overwrite - ).toEqual(false); - }); - - test('overwrite validates with a boolean true', () => { - expect( - importRulesQuerySchema.validate>({ - overwrite: true, - }).error - ).toBeFalsy(); - }); - - test('overwrite does not validate with a weird string', () => { - expect( - importRulesQuerySchema.validate< - Partial< - Omit & { - overwrite: string; - } - > - >({ - overwrite: 'blah', - }).error - ).toBeTruthy(); - }); - }); - - describe('importRulesPayloadSchema', () => { - test('does not validate with an empty object', () => { - expect(importRulesPayloadSchema.validate({}).error).toBeTruthy(); - }); - - test('does validate with a file object', () => { - expect(importRulesPayloadSchema.validate({ file: {} }).error).toBeFalsy(); - }); - }); - - test('The default for "from" will be "now-6m"', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.from - ).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - language: 'kuery', - max_signals: 1, - version: 1, - }).value.to - ).toEqual('now'); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' - ); - }); - - test('The default for "actions" will be an empty array', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.actions - ).toEqual([]); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - expect( - importRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - expect( - importRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - expect( - importRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - expect( - importRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are including "actionTypeId', () => { - expect( - importRulesSchema.validate< - Partial< - Omit & { - actions: AlertAction[]; - } - > - >({ - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('The default for "throttle" will be null', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.throttle - ).toEqual(null); - }); - - describe('note', () => { - test('You can set note to a string', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - note: '# test header', - }).error - ).toBeFalsy(); - }); - - test('You can set note to an empty string', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - note: '', - }).error - ).toBeFalsy(); - }); - - test('You cannot create note set to null', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - note: null, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - - test('You cannot create note as something other than a string', () => { - expect( - importRulesSchema.validate & { note: object }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: false, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - note: { - somethingMadeUp: { somethingElse: true }, - }, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - }); - - // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema - // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the - // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, - // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('exceptions_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - expect( - importRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate and exceptions_list is empty', () => { - expect( - importRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [{ invalid_value: 'invalid value' }], - }).error.message - ).toEqual( - 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' - ); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate', () => { - expect( - importRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }).value.exceptions_list - ).toEqual([]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/import_rules_schema.ts deleted file mode 100644 index d3c728ebac1a9..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ /dev/null @@ -1,131 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { - id, - actions, - created_at, - updated_at, - created_by, - updated_by, - enabled, - description, - false_positives, - filters, - from, - immutable, - index, - rule_id, - interval, - query, - language, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - throttle, - references, - note, - version, - lists, - anomaly_threshold, - machine_learning_job_id, -} from './schemas'; -/* eslint-enable @typescript-eslint/camelcase */ - -import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; -import { hasListsFeature } from '../../feature_flags'; - -/** - * Differences from this and the createRulesSchema are - * - rule_id is required - * - id is optional (but ignored in the import code - rule_id is exclusively used for imports) - * - created_at is optional (but ignored in the import code) - * - updated_at is optional (but ignored in the import code) - * - created_by is optional (but ignored in the import code) - * - updated_by is optional (but ignored in the import code) - */ -export const importRulesSchema = Joi.object({ - anomaly_threshold: anomaly_threshold.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - id, - actions: actions.default([]), - description: description.required(), - enabled: enabled.default(true), - false_positives: false_positives.default([]), - filters, - from: from.default('now-6m'), - rule_id: rule_id.required(), - immutable: immutable.default(false).valid(false), - index, - interval: interval.default('5m'), - query: query.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: query.allow('').default(''), - }), - language: language.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: language.default('kuery'), - }), - output_index, - machine_learning_job_id: machine_learning_job_id.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - saved_id: saved_id.when('type', { - is: 'saved_query', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - timeline_id, - timeline_title, - meta, - risk_score: risk_score.required(), - max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), - name: name.required(), - severity: severity.required(), - tags: tags.default([]), - to: to.default('now'), - type: type.required(), - threat: threat.default([]), - throttle: throttle.default(null), - references: references.default([]), - note: note.allow(''), - version: version.default(1), - created_at, - updated_at, - created_by, - updated_by, - - // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), -}); - -export const importRulesQuerySchema = Joi.object({ - overwrite: Joi.boolean().default(false), -}); - -export const importRulesPayloadSchema = Joi.object({ - file: Joi.object().required(), -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts deleted file mode 100644 index e87c732e8a2f7..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts +++ /dev/null @@ -1,100 +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 { patchRulesBulkSchema } from './patch_rules_bulk_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -// only the basics of testing are here. -// see: patch_rules_schema.test.ts for the bulk of the validation tests -// this just wraps patchRulesSchema in an array -describe('patch_rules_bulk_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('can take an empty array and validate it', () => { - expect( - patchRulesBulkSchema.validate>>([]).error - ).toBeFalsy(); - }); - - test('made up values do not validate', () => { - expect( - patchRulesBulkSchema.validate<[{ madeUp: string }]>([ - { - madeUp: 'hi', - }, - ]).error - ).toBeTruthy(); - }); - - test('single array of [id] does validate', () => { - expect( - patchRulesBulkSchema.validate>>([ - { - id: 'rule-1', - }, - ]).error - ).toBeFalsy(); - }); - - test('two values of [id] does validate', () => { - expect( - patchRulesBulkSchema.validate>>([ - { - id: 'rule-1', - }, - { - id: 'rule-2', - }, - ]).error - ).toBeFalsy(); - }); - - test('can set "note" to be a string', () => { - expect( - patchRulesBulkSchema.validate>>([ - { - id: 'rule-1', - note: 'hi', - }, - ]).error - ).toBeFalsy(); - }); - - test('can set "note" to be an empty string', () => { - expect( - patchRulesBulkSchema.validate>>([ - { - id: 'rule-1', - note: '', - }, - ]).error - ).toBeFalsy(); - }); - - test('cannot set "note" to be anything other than a string', () => { - expect( - patchRulesBulkSchema.validate< - Array & { note: object }>> - >([ - { - id: 'rule-1', - note: { - someprop: 'some value here', - }, - }, - ]).error.message - ).toEqual( - '"value" at position 0 fails because [child "note" fails because ["note" must be a string]]' - ); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts deleted file mode 100644 index ff813bce84add..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -import { patchRulesSchema } from './patch_rules_schema'; - -export const patchRulesBulkSchema = Joi.array().items(patchRulesSchema); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts deleted file mode 100644 index 218cae68db036..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ /dev/null @@ -1,1362 +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 { AlertAction } from '../../../../../../alerts/common'; -import { patchRulesSchema } from './patch_rules_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { ThreatParams } from '../../types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('patch rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('empty objects do not validate as they require at least id or rule_id', () => { - expect(patchRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - patchRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[id] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[rule_id] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[id] and [rule_id] does not validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'id-1', - rule_id: 'rule-1', - }).error.message - ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); - }); - - test('[rule_id, description] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, description] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, risk_score] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - risk_score: 10, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name, severity] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name, severity] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name, severity, type] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name, severity, type, interval] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as a valid value to patch with', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('does not default references to an array', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).value.references - ).toEqual(undefined); - }); - - test('does not default interval', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - }).value.interval - ).toEqual(undefined); - }); - - test('does not default max signal', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).value.max_signals - ).toEqual(undefined); - }); - - test('references cannot be numbers', () => { - expect( - patchRulesSchema.validate< - Partial> & { references: number[] } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - references: [5], - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('indexes cannot be numbers', () => { - expect( - patchRulesSchema.validate< - Partial> & { index: number[] } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('saved_id is not required when type is saved_query and will validate without it', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - }).error - ).toBeFalsy(); - }); - - test('saved_id validates with saved_query', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); - }); - - test('language validates with kuery', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'lucene', - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - patchRulesSchema.validate< - Partial & { language: string }> - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - }).error.message - ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); - }); - - test('max_signals cannot be negative', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals cannot be zero', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals can be 1', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('meta can be patched', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - meta: { whateverYouWant: 'anything_at_all' }, - }).error - ).toBeFalsy(); - }); - - test('You cannot patch meta as a string', () => { - expect( - patchRulesSchema.validate & { meta: string }>>( - { - id: 'rule-1', - meta: 'should not work', - } - ).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('filters cannot be a string', () => { - expect( - patchRulesSchema.validate< - Partial & { filters: string }> - >({ - rule_id: 'rule-1', - type: 'query', - filters: 'some string', - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('threat is not defaulted to empty array on patch', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).value.threat - ).toBe(undefined); - }); - - test('threat is not defaulted to undefined on patch with empty array', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [], - }).value.threat - ).toMatchObject([]); - }); - - test('threat is valid when updated with all sub-objects', () => { - const expected: ThreatParams[] = [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ]; - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).value.threat - ).toMatchObject(expected); - }); - - test('threat is invalid when updated with missing property framework', () => { - expect( - patchRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' - ); - }); - - test('threat is invalid when updated with missing tactic sub-object', () => { - expect( - patchRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' - ); - }); - - test('threat is invalid when updated with missing technique', () => { - expect( - patchRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' - ); - }); - - test('validates with timeline_id and timeline_title', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', - timeline_title: 'some-title', - }).error - ).toBeFalsy(); - }); - - test('You cannot omit timeline_title when timeline_id is present', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); - }); - - test('You cannot have a null value for timeline_title when timeline_id is present', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'timeline-id', - timeline_title: null, - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); - }); - - test('You cannot have empty string for timeline_title when timeline_id is present', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: 'some-id', - timeline_title: '', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title with an empty timeline_id', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_id: '', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title without timeline_id', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' - ); - }); - - describe('note', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, note] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - note: '# some documentation markdown', - }).error - ).toBeFalsy(); - }); - - test('note can be patched', () => { - expect( - patchRulesSchema.validate>({ - id: 'rule-1', - note: '# new documentation markdown', - }).error - ).toBeFalsy(); - }); - - test('You cannot patch note as an object', () => { - expect( - patchRulesSchema.validate< - Partial & { note: object }> - >({ - id: 'rule-1', - note: { - someProperty: 'something else here', - }, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - expect( - patchRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - expect( - patchRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - expect( - patchRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - expect( - patchRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are including "actionTypeId', () => { - expect( - patchRulesSchema.validate< - Partial< - Omit & { - actions: AlertAction[]; - } - > - >({ - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema - // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the - // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, - // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('exceptions_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('exceptions_list can be patched', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'some id', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - ], - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - expect( - patchRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - expect( - patchRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [{ invalid_value: 'invalid value' }], - }).error.message - ).toEqual( - 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' - ); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - expect( - patchRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }).value.exceptions_list - ).toEqual([]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts deleted file mode 100644 index 503bc64df237c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ /dev/null @@ -1,82 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { - actions, - enabled, - description, - false_positives, - filters, - from, - index, - rule_id, - interval, - query, - language, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - throttle, - references, - note, - id, - version, - lists, - anomaly_threshold, - machine_learning_job_id, -} from './schemas'; -import { hasListsFeature } from '../../feature_flags'; -/* eslint-enable @typescript-eslint/camelcase */ - -export const patchRulesSchema = Joi.object({ - actions, - anomaly_threshold, - description, - enabled, - false_positives, - filters, - from, - rule_id, - id, - index, - interval, - query: query.allow(''), - language, - machine_learning_job_id, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - throttle, - references, - note: note.allow(''), - version, - - // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), -}).xor('id', 'rule_id'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts deleted file mode 100644 index 389c5ff7ea617..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts +++ /dev/null @@ -1,97 +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 { queryRulesBulkSchema } from './query_rules_bulk_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -// only the basics of testing are here. -// see: query_rules_bulk_schema.test.ts for the bulk of the validation tests -// this just wraps queryRulesSchema in an array -describe('query_rules_bulk_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('can take an empty array and validate it', () => { - expect( - queryRulesBulkSchema.validate>>([]).error - ).toBeFalsy(); - }); - - test('both rule_id and id being supplied do not validate', () => { - expect( - queryRulesBulkSchema.validate>>([ - { - rule_id: '1', - id: '1', - }, - ]).error.message - ).toEqual( - '"value" at position 0 fails because ["value" contains a conflict between exclusive peers [id, rule_id]]' - ); - }); - - test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => { - expect( - queryRulesBulkSchema.validate>>([ - { - id: '1', - }, - { - rule_id: '1', - id: '1', - }, - ]).error.message - ).toEqual( - '"value" at position 1 fails because ["value" contains a conflict between exclusive peers [id, rule_id]]' - ); - }); - - test('only id validates', () => { - expect( - queryRulesBulkSchema.validate>>([{ id: '1' }]).error - ).toBeFalsy(); - }); - - test('only id validates with two elements', () => { - expect( - queryRulesBulkSchema.validate>>([ - { id: '1' }, - { id: '2' }, - ]).error - ).toBeFalsy(); - }); - - test('only rule_id validates', () => { - expect( - queryRulesBulkSchema.validate>>([{ rule_id: '1' }]) - .error - ).toBeFalsy(); - }); - - test('only rule_id validates with two elements', () => { - expect( - queryRulesBulkSchema.validate>>([ - { rule_id: '1' }, - { rule_id: '2' }, - ]).error - ).toBeFalsy(); - }); - - test('both id and rule_id validates with two separate elements', () => { - expect( - queryRulesBulkSchema.validate>>([ - { id: '1' }, - { rule_id: '2' }, - ]).error - ).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts deleted file mode 100644 index 13ccac282281d..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts +++ /dev/null @@ -1,10 +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 Joi from 'joi'; - -import { queryRulesSchema } from './query_rules_schema'; - -export const queryRulesBulkSchema = Joi.array().items(queryRulesSchema); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts deleted file mode 100644 index 68be4c627780c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts +++ /dev/null @@ -1,42 +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 { queryRulesSchema } from './query_rules_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('queryRulesSchema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('empty objects do not validate', () => { - expect(queryRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('both rule_id and id being supplied do not validate', () => { - expect( - queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error - .message - ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); - }); - - test('only id validates', () => { - expect( - queryRulesSchema.validate>({ id: '1' }).error - ).toBeFalsy(); - }); - - test('only rule_id validates', () => { - expect( - queryRulesSchema.validate>({ rule_id: '1' }).error - ).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts deleted file mode 100644 index 4752d1794ff28..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts +++ /dev/null @@ -1,75 +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 { querySignalsSchema } from './query_signals_index_schema'; -import { SignalsQueryRestParams } from '../../signals/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('query, aggs, size, _source and track_total_hits on signals index', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('query, aggs, size, _source and track_total_hits simultaneously', () => { - expect( - querySignalsSchema.validate>({ - query: {}, - aggs: {}, - size: 1, - track_total_hits: true, - _source: ['field'], - }).error - ).toBeFalsy(); - }); - - test('query only', () => { - expect( - querySignalsSchema.validate>({ - query: {}, - }).error - ).toBeFalsy(); - }); - - test('aggs only', () => { - expect( - querySignalsSchema.validate>({ - aggs: {}, - }).error - ).toBeFalsy(); - }); - - test('size only', () => { - expect( - querySignalsSchema.validate>({ - size: 1, - }).error - ).toBeFalsy(); - }); - - test('track_total_hits only', () => { - expect( - querySignalsSchema.validate>({ - track_total_hits: true, - }).error - ).toBeFalsy(); - }); - - test('_source only', () => { - expect( - querySignalsSchema.validate>({ - _source: ['field'], - }).error - ).toBeFalsy(); - }); - - test('missing query, aggs, size, _source and track_total_hits is invalid', () => { - expect(querySignalsSchema.validate>({}).error).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts deleted file mode 100644 index 26a32d2e4980b..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts +++ /dev/null @@ -1,15 +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 Joi from 'joi'; - -export const querySignalsSchema = Joi.object({ - query: Joi.object(), - aggs: Joi.object(), - size: Joi.number().integer(), - track_total_hits: Joi.boolean(), - _source: Joi.array().items(Joi.string()), -}).min(1); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/schemas.ts deleted file mode 100644 index ae951ea5e33d2..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/schemas.ts +++ /dev/null @@ -1,125 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -export const anomaly_threshold = Joi.number().integer().greater(-1).less(101); -export const description = Joi.string(); -export const enabled = Joi.boolean(); -export const exclude_export_details = Joi.boolean(); -export const false_positives = Joi.array().items(Joi.string()); -export const file_name = Joi.string(); -export const filters = Joi.array(); -export const from = Joi.string(); -export const immutable = Joi.boolean(); -export const rule_id = Joi.string(); -export const id = Joi.string(); -export const index = Joi.array().items(Joi.string()).single(); -export const interval = Joi.string(); -export const query = Joi.string(); -export const language = Joi.string().valid('kuery', 'lucene'); -export const objects = Joi.array().items( - Joi.object({ - rule_id, - }).required() -); -export const output_index = Joi.string(); -export const saved_id = Joi.string(); -export const timeline_id = Joi.string(); -export const timeline_title = Joi.string().when('timeline_id', { - is: Joi.exist(), - then: Joi.required(), - otherwise: Joi.forbidden(), -}); -export const meta = Joi.object(); -export const max_signals = Joi.number().integer().greater(0); -export const name = Joi.string(); -export const risk_score = Joi.number().integer().greater(-1).less(101); -export const severity = Joi.string().valid('low', 'medium', 'high', 'critical'); -export const status = Joi.string().valid('open', 'closed'); -export const to = Joi.string(); -export const type = Joi.string().valid('query', 'saved_query', 'machine_learning'); -export const machine_learning_job_id = Joi.string(); -export const queryFilter = Joi.string(); -export const references = Joi.array().items(Joi.string()).single(); -export const per_page = Joi.number().integer().min(0).default(20); -export const page = Joi.number().integer().min(1).default(1); -export const signal_ids = Joi.array().items(Joi.string()); -export const signal_status_query = Joi.object(); -export const sort_field = Joi.string(); -export const sort_order = Joi.string().valid('asc', 'desc'); -export const tags = Joi.array().items(Joi.string()); -export const fields = Joi.array().items(Joi.string()).single(); -export const threat_framework = Joi.string(); -export const threat_tactic_id = Joi.string(); -export const threat_tactic_name = Joi.string(); -export const threat_tactic_reference = Joi.string(); -export const threat_tactic = Joi.object({ - id: threat_tactic_id.required(), - name: threat_tactic_name.required(), - reference: threat_tactic_reference.required(), -}); -export const threat_technique_id = Joi.string(); -export const threat_technique_name = Joi.string(); -export const threat_technique_reference = Joi.string(); -export const threat_technique = Joi.object({ - id: threat_technique_id.required(), - name: threat_technique_name.required(), - reference: threat_technique_reference.required(), -}); -export const threat_techniques = Joi.array().items(threat_technique.required()); -export const threat = Joi.array().items( - Joi.object({ - framework: threat_framework.required(), - tactic: threat_tactic.required(), - technique: threat_techniques.required(), - }) -); -export const created_at = Joi.string().isoDate().strict(); -export const updated_at = Joi.string().isoDate().strict(); -export const created_by = Joi.string(); -export const updated_by = Joi.string(); -export const version = Joi.number().integer().min(1); -export const action_group = Joi.string(); -export const action_id = Joi.string(); -export const action_action_type_id = Joi.string(); -export const action_params = Joi.object(); -export const action = Joi.object({ - group: action_group.required(), - id: action_id.required(), - action_type_id: action_action_type_id.required(), - params: action_params.required(), -}); -export const actions = Joi.array().items(action); -export const throttle = Joi.string().allow(null); -export const note = Joi.string(); - -// NOTE: Experimental list support not being shipped currently and behind a feature flag -// TODO: (LIST-FEATURE) Remove this comment once we lists have passed testing and is ready for the release -export const list_field = Joi.string(); -export const list_values_operator = Joi.string().valid(['included', 'excluded']); -export const list_values_types = Joi.string().valid(['match', 'match_all', 'list', 'exists']); -export const list_values = Joi.object({ - name: Joi.string().required(), - id: Joi.string(), - description: Joi.string(), - created_at, -}); -export const list = Joi.object({ - field: list_field.required(), - values_operator: list_values_operator.required(), - values_type: list_values_types.required(), - values: Joi.when('values_type', { - is: 'exists', - then: Joi.forbidden(), - otherwise: Joi.array().items(list_values).required(), - }), -}); -export const list_and = Joi.object({ - and: Joi.array().items(list), -}); -export const lists = Joi.array().items(list.concat(list_and)); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts deleted file mode 100644 index 953532a6e1c26..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts +++ /dev/null @@ -1,75 +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 { setSignalsStatusSchema } from './set_signal_status_schema'; -import { SignalsStatusRestParams } from '../../signals/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('set signal status schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('signal_ids and status is valid', () => { - expect( - setSignalsStatusSchema.validate>({ - signal_ids: ['somefakeid'], - status: 'open', - }).error - ).toBeFalsy(); - }); - - test('query and status is valid', () => { - expect( - setSignalsStatusSchema.validate>({ - query: {}, - status: 'open', - }).error - ).toBeFalsy(); - }); - - test('signal_ids and missing status is invalid', () => { - expect( - setSignalsStatusSchema.validate>({ - signal_ids: ['somefakeid'], - }).error.message - ).toEqual('child "status" fails because ["status" is required]'); - }); - - test('query and missing status is invalid', () => { - expect( - setSignalsStatusSchema.validate>({ - query: {}, - }).error.message - ).toEqual('child "status" fails because ["status" is required]'); - }); - - test('status is present but query or signal_ids is missing is invalid', () => { - expect( - setSignalsStatusSchema.validate>({ - status: 'closed', - }).error.message - ).toEqual('"value" must contain at least one of [signal_ids, query]'); - }); - - test('signal_ids is present but status has wrong value', () => { - expect( - setSignalsStatusSchema.validate< - Partial< - Omit & { - status: string; - } - > - >({ - status: 'fakeVal', - }).error.message - ).toEqual('child "status" fails because ["status" must be one of [open, closed]]'); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts deleted file mode 100644 index c8a06619287df..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts +++ /dev/null @@ -1,17 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { signal_ids, signal_status_query, status } from './schemas'; -/* eslint-enable @typescript-eslint/camelcase */ - -export const setSignalsStatusSchema = Joi.object({ - signal_ids, - query: signal_status_query, - status: status.required(), -}).xor('signal_ids', 'query'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts deleted file mode 100644 index d329070eaaa0a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts +++ /dev/null @@ -1,91 +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 { updateRulesBulkSchema } from './update_rules_bulk_schema'; -import { UpdateRuleAlertParamsRest } from '../../rules/types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -// only the basics of testing are here. -// see: update_rules_schema.test.ts for the bulk of the validation tests -// this just wraps updateRulesSchema in an array -describe('update_rules_bulk_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('can take an empty array and validate it', () => { - expect( - updateRulesBulkSchema.validate>>([]).error - ).toBeFalsy(); - }); - - test('made up values do not validate', () => { - expect( - updateRulesBulkSchema.validate<[{ madeUp: string }]>([ - { - madeUp: 'hi', - }, - ]).error - ).toBeTruthy(); - }); - - test('single array of [id] does validate', () => { - expect( - updateRulesBulkSchema.validate>>([ - { - id: 'id-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }, - ]).error - ).toBeFalsy(); - }); - - test('two values of [id] does validate', () => { - expect( - updateRulesBulkSchema.validate>>([ - { - id: 'id-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }, - { - id: 'id-2', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }, - ]).error - ).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts deleted file mode 100644 index 123ec2d5b7e15..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -import { updateRulesSchema } from './update_rules_schema'; - -export const updateRulesBulkSchema = Joi.array().items(updateRulesSchema); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts deleted file mode 100644 index 8bda16de97775..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ /dev/null @@ -1,1645 +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 { AlertAction } from '../../../../../../alerts/common'; -import { updateRulesSchema } from './update_rules_schema'; -import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { ThreatParams, RuleAlertParamsRest } from '../../types'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; - -describe('create rules schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('empty objects do not validate as they require at least id or rule_id', () => { - expect(updateRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - updateRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[rule_id] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeTruthy(); - }); - - test('[id] and [rule_id] does not validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'id-1', - 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', - }).error.message - ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]'); - }); - - test('[rule_id, description] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - expect( - updateRulesSchema.validate>({ - 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', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - expect( - updateRulesSchema.validate>({ - 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', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('You can send in an empty array to threat', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as valid', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('defaults references to an array', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).value.references - ).toEqual([]); - }); - - test('references cannot be numbers', () => { - expect( - updateRulesSchema.validate< - Partial> & { references: number[] } - >({ - 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: [5], - }).error.message - ).toEqual( - 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('indexes cannot be numbers', () => { - expect( - updateRulesSchema.validate> & { index: number[] }>( - { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - } - ).error.message - ).toEqual( - 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('defaults interval to 5 min', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - type: 'query', - }).value.interval - ).toEqual('5m'); - }); - - test('defaults max signals to 100', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }).value.max_signals - ).toEqual(100); - }); - - test('saved_id is required when type is saved_query and will not validate without out', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - }).error.message - ).toEqual('child "saved_id" fails because ["saved_id" is required]'); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); - }); - - test('filters cannot be a string', () => { - expect( - updateRulesSchema.validate< - Partial & { filters: string }> - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: 'some string', - }).error.message - ).toEqual('child "filters" fails because ["filters" must be an array]'); - }); - - test('language validates with kuery', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'lucene', - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - updateRulesSchema.validate< - Partial & { language: string }> - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - }).error.message - ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]'); - }); - - test('max_signals cannot be negative', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals cannot be zero', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - }).error.message - ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]'); - }); - - test('max_signals can be 1', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can optionally send in an array of tags', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: ['tag_1', 'tag_2'], - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of tags that are numbers', () => { - expect( - updateRulesSchema.validate> & { tags: number[] }>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - }).error.message - ).toEqual( - 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]' - ); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threat: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }).error.message - ).toEqual( - 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]' - ); - }); - - test('You can optionally send in an array of false positives', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: ['false_1', 'false_2'], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - expect( - updateRulesSchema.validate< - Partial> & { false_positives: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: [5, 4], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual( - 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]' - ); - }); - - test('You cannot set the immutable when trying to create a rule', () => { - expect( - updateRulesSchema.validate< - Partial> & { immutable: number } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: 5, - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('"immutable" is not allowed'); - }); - - test('You cannot set the risk_score to 101', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 101, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]'); - }); - - test('You cannot set the risk_score to -1', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: -1, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error.message - ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]'); - }); - - test('You can set the risk_score to 0', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 0, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set the risk_score to 100', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 100, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set meta to any object you want', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }).error - ).toBeFalsy(); - }); - - test('You cannot create meta as a string', () => { - expect( - updateRulesSchema.validate & { meta: string }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: 'should not work', - }).error.message - ).toEqual('child "meta" fails because ["meta" must be an object]'); - }); - - test('You can omit the query string when filters are present', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('validates with timeline_id and timeline_title', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }).error - ).toBeFalsy(); - }); - - test('You cannot omit timeline_title when timeline_id is present', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is required]'); - }); - - test('You cannot have a null value for timeline_title when timeline_id is present', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - timeline_title: null, - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]'); - }); - - test('You cannot have empty string for timeline_title when timeline_id is present', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: 'some_id', - timeline_title: '', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title with an empty timeline_id', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_id: '', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]'); - }); - - test('You cannot have timeline_title without timeline_id', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - timeline_title: 'some-title', - }).error.message - ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]'); - }); - - test('The default for "from" will be "now-6m"', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.from - ).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.to - ).toEqual('now'); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'junk', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]' - ); - }); - - test('The default for "actions" will be an empty array', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.actions - ).toEqual([]); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - expect( - updateRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "group" fails because ["group" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - expect( - updateRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "id" fails because ["id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - expect( - updateRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', params: {} }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - expect( - updateRulesSchema.validate< - Partial< - Omit & { - actions: Array>; - } - > - >({ - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "params" fails because ["params" is required]]]' - ); - }); - - test('You cannot send in an array of actions that are including "actionTypeId"', () => { - expect( - updateRulesSchema.validate< - Partial< - Omit & { - actions: AlertAction[]; - } - > - >({ - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).error.message - ).toEqual( - 'child "actions" fails because ["actions" at position 0 fails because [child "action_type_id" fails because ["action_type_id" is required]]]' - ); - }); - - test('The default for "throttle" will be null', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - name: 'some-name', - severity: 'low', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - version: 1, - }).value.throttle - ).toEqual(null); - }); - - describe('note', () => { - test('You can set note to a string', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: '# some documentation title', - }).error - ).toBeFalsy(); - }); - - test('You can set note to an empty string', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: '', - }).error - ).toBeFalsy(); - }); - - // Note: If you're looking to remove `note`, omit `note` entirely - test('You cannot set note to null', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: null, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - - test('You cannot set note as an object', () => { - expect( - updateRulesSchema.validate & { note: object }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - note: { - somethingMadeUp: { somethingElse: true }, - }, - }).error.message - ).toEqual('child "note" fails because ["note" must be a string]'); - }); - }); - - // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema - // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the - // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, - // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally - describe.skip('exceptions_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - ], - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [], - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - expect( - updateRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - exceptions_list: [{ invalid_value: 'invalid value' }], - }).error.message - ).toEqual( - 'child "exceptions_list" fails because ["exceptions_list" at position 0 fails because [child "field" fails because ["field" is required]]]' - ); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - expect( - updateRulesSchema.validate>>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }).value.exceptions_list - ).toEqual([]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_schema.ts deleted file mode 100644 index b1b37801b644f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ /dev/null @@ -1,111 +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 Joi from 'joi'; - -/* eslint-disable @typescript-eslint/camelcase */ -import { - actions, - enabled, - description, - false_positives, - filters, - from, - index, - rule_id, - interval, - query, - language, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threat, - throttle, - references, - id, - note, - version, - lists, - anomaly_threshold, - machine_learning_job_id, -} from './schemas'; -/* eslint-enable @typescript-eslint/camelcase */ - -import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; -import { hasListsFeature } from '../../feature_flags'; - -/** - * This almost identical to the create_rules_schema except for a few details. - * - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented - * This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in - * this will update the rule's version number. - * - id is on here because you can pass in an id to update using it instead of rule_id. - */ -export const updateRulesSchema = Joi.object({ - actions: actions.default([]), - anomaly_threshold: anomaly_threshold.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - description: description.required(), - enabled: enabled.default(true), - id, - false_positives: false_positives.default([]), - filters, - from: from.default('now-6m'), - rule_id, - index, - interval: interval.default('5m'), - query: query.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: query.allow('').default(''), - }), - language: language.when('type', { - is: 'machine_learning', - then: Joi.forbidden(), - otherwise: language.default('kuery'), - }), - machine_learning_job_id: machine_learning_job_id.when('type', { - is: 'machine_learning', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - output_index, - saved_id: saved_id.when('type', { - is: 'saved_query', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - timeline_id, - timeline_title, - meta, - risk_score: risk_score.required(), - max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), - name: name.required(), - severity: severity.required(), - tags: tags.default([]), - to: to.default('now'), - type: type.required(), - threat: threat.default([]), - throttle: throttle.default(null), - references: references.default([]), - note: note.allow(''), - version, - - // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release - exceptions_list: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), -}).xor('id', 'rule_id'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index a8819bbd7432c..f3109a911203d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -101,11 +101,12 @@ describe('set signal status', () => { path: DETECTION_ENGINE_SIGNALS_STATUS_URL, body: setStatusSignalMissingIdsAndQueryPayload(), }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith( - '"value" must contain at least one of [signal_ids, query]' - ); + const response = await server.inject(request, context); + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: ['either "signal_ids" or "query" must be set'], + status_code: 400, + }); }); test('rejects if signal_ids but no status', async () => { @@ -118,7 +119,7 @@ describe('set signal status', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "status" fails because ["status" is required]' + 'Invalid value "undefined" supplied to "status"' ); }); @@ -132,7 +133,7 @@ describe('set signal status', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "status" fails because ["status" is required]' + 'Invalid value "undefined" supplied to "status"' ); }); @@ -150,7 +151,7 @@ describe('set signal status', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "status" fails because ["status" is required]' + 'Invalid value "undefined" supplied to "status"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index e3e19d83d8f0d..18c3440ab93e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -4,18 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setSignalStatusValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/set_signal_status_type_dependents'; +import { + SetSignalsStatusSchemaDecoded, + setSignalsStatusSchema, +} from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; -import { SignalsStatusRestParams } from '../../signals/types'; -import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; -import { transformError, buildRouteValidation, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; export const setSignalsStatusRoute = (router: IRouter) => { router.post( { path: DETECTION_ENGINE_SIGNALS_STATUS_URL, validate: { - body: buildRouteValidation(setSignalsStatusSchema), + body: buildRouteValidation( + setSignalsStatusSchema + ), }, options: { tags: ['access:securitySolution'], @@ -26,6 +32,10 @@ export const setSignalsStatusRoute = (router: IRouter) => { const clusterClient = context.core.elasticsearch.legacy.client; const siemClient = context.securitySolution?.getAppClient(); const siemResponse = buildSiemResponse(response); + const validationErrors = setSignalStatusValidateTypeDependents(request.body); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } if (!siemClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 8d7b171a8537b..f0863164a3f23 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -128,9 +128,12 @@ describe('query for signal', () => { path: DETECTION_ENGINE_QUERY_SIGNALS_URL, body: {}, }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith('"value" must have at least 1 children'); + const response = await server.inject(request, context); + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: '"value" must have at least 1 children', + status_code: 400, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts index a93f4ebff58d4..5f62ff426ecd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -6,16 +6,22 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; -import { SignalsQueryRestParams } from '../../signals/types'; -import { querySignalsSchema } from '../schemas/query_signals_index_schema'; -import { transformError, buildRouteValidation, buildSiemResponse } from '../utils'; +import { transformError, buildSiemResponse } from '../utils'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; + +import { + querySignalsSchema, + QuerySignalsSchemaDecoded, +} from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; export const querySignalsRoute = (router: IRouter) => { router.post( { path: DETECTION_ENGINE_QUERY_SIGNALS_URL, validate: { - body: buildRouteValidation(querySignalsSchema), + body: buildRouteValidation( + querySignalsSchema + ), }, options: { tags: ['access:securitySolution'], @@ -23,9 +29,22 @@ export const querySignalsRoute = (router: IRouter) => { }, async (context, request, response) => { const { query, aggs, _source, track_total_hits, size } = request.body; + const siemResponse = buildSiemResponse(response); + if ( + query == null && + aggs == null && + _source == null && + // eslint-disable-next-line @typescript-eslint/camelcase + track_total_hits == null && + size == null + ) { + return siemResponse.error({ + statusCode: 400, + body: '"value" must have at least 1 children', + }); + } const clusterClient = context.core.elasticsearch.legacy.client; const siemClient = context.securitySolution!.getAppClient(); - const siemResponse = buildSiemResponse(response); try { const result = await clusterClient.callAsCurrentUser('search', { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts new file mode 100644 index 0000000000000..d00bffb96ad05 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -0,0 +1,78 @@ +/* + * 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 { CreateRulesOptions } from './types'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; + +export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ + alertsClient: alertsClientMock.create(), + anomalyThreshold: undefined, + description: 'some description', + enabled: true, + falsePositives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + query: 'user.name: root or user.name: admin', + language: 'kuery', + savedId: 'savedId-123', + timelineId: 'timelineid-123', + timelineTitle: 'timeline-title-123', + meta: {}, + machineLearningJobId: undefined, + filters: [], + ruleId: 'rule-1', + immutable: false, + index: ['index-123'], + interval: '5m', + maxSignals: 100, + riskScore: 80, + outputIndex: 'output-1', + name: 'Query with a rule id', + severity: 'high', + tags: [], + threat: [], + to: 'now', + type: 'query', + references: ['http://www.example.com'], + note: '# sample markdown', + version: 1, + exceptionsList: [], + actions: [], +}); + +export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ + alertsClient: alertsClientMock.create(), + anomalyThreshold: 55, + description: 'some description', + enabled: true, + falsePositives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + query: undefined, + language: undefined, + savedId: 'savedId-123', + timelineId: 'timelineid-123', + timelineTitle: 'timeline-title-123', + meta: {}, + machineLearningJobId: 'new_job_id', + filters: [], + ruleId: 'rule-1', + immutable: false, + index: ['index-123'], + interval: '5m', + maxSignals: 100, + riskScore: 80, + outputIndex: 'output-1', + name: 'Machine Learning Job', + severity: 'high', + tags: [], + threat: [], + to: 'now', + type: 'machine_learning', + references: ['http://www.example.com'], + note: '# sample markdown', + version: 1, + exceptionsList: [], + actions: [], +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts index f086166d0685e..b58d64c86142d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.test.ts @@ -4,36 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerts/server/mocks'; -import { getMlResult } from '../routes/__mocks__/request_responses'; import { createRules } from './create_rules'; +import { getCreateMlRulesOptionsMock } from './create_rules.mock'; describe('createRules', () => { - let alertsClient: ReturnType; - - beforeEach(() => { - alertsClient = alertsClientMock.create(); - }); - it('calls the alertsClient with ML params', async () => { - const params = { - ...getMlResult().params, - anomalyThreshold: 55, - machineLearningJobId: 'new_job_id', - }; - - await createRules({ - alertsClient, - ...params, - ruleId: 'new-rule-id', - enabled: true, - interval: '', - name: '', - tags: [], - actions: [], - }); - - expect(alertsClient.create).toHaveBeenCalledWith( + const ruleOptions = getCreateMlRulesOptionsMock(); + await createRules(ruleOptions); + expect(ruleOptions.alertsClient.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 67e066c6670fb..83e9b0de16f06 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -7,7 +7,7 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { Alert } from '../../../../../alerts/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; -import { CreateRuleParams } from './types'; +import { CreateRulesOptions } from './types'; import { addTags } from './add_tags'; import { hasListsFeature } from '../feature_flags'; @@ -42,11 +42,11 @@ export const createRules = async ({ references, note, version, - exceptions_list, + exceptionsList, actions, -}: CreateRuleParams): Promise => { +}: CreateRulesOptions): Promise => { // TODO: Remove this and use regular exceptions_list once the feature is stable for a release - const exceptionsListParam = hasListsFeature() ? { exceptions_list } : {}; + const exceptionsListParam = hasListsFeature() ? { exceptionsList } : {}; return alertsClient.create({ data: { name, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index 8044692ab90b1..73d3c65774b3d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -6,12 +6,12 @@ import { Readable } from 'stream'; import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; import { createPromiseFromStreams } from 'src/legacy/utils/streams'; -import { ImportRuleAlertRest } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; +import { ImportRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/import_rules_schema'; -type PromiseFromStreams = ImportRuleAlertRest | Error; +type PromiseFromStreams = ImportRulesSchemaDecoded | Error; -export const getOutputSample = (): Partial => ({ +export const getOutputSample = (): Partial => ({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -25,7 +25,7 @@ export const getOutputSample = (): Partial => ({ type: 'query', }); -export const getSampleAsNdjson = (sample: Partial): string => { +export const getSampleAsNdjson = (sample: Partial): string => { return `${JSON.stringify(sample)}\n`; }; @@ -64,8 +64,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', exceptions_list: [], max_signals: 100, tags: [], @@ -90,8 +88,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', exceptions_list: [], max_signals: 100, tags: [], @@ -154,8 +150,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, tags: [], exceptions_list: [], @@ -180,8 +174,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], @@ -227,8 +219,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], @@ -253,8 +243,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], @@ -300,8 +288,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], @@ -327,8 +313,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], @@ -373,8 +357,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], @@ -383,8 +365,9 @@ describe('create_rules_stream_from_ndjson', () => { references: [], version: 1, }); + // TODO: Change the formatter to output something better than [object Object] expect(resultOrError[1].message).toEqual( - 'child "description" fails because ["description" is required]' + '[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]' ); expect(resultOrError[2]).toEqual({ actions: [], @@ -402,8 +385,6 @@ describe('create_rules_stream_from_ndjson', () => { enabled: true, false_positives: [], immutable: false, - query: '', - language: 'kuery', max_signals: 100, exceptions_list: [], tags: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 034813b8d100d..932a4ef9eed92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -4,13 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { Transform } from 'stream'; -import { ImportRuleAlertRest } from '../types'; +import * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { importRuleValidateTypeDependents } from '../../../../common/detection_engine/schemas/request/import_rules_type_dependents'; +import { exactCheck } from '../../../../common/exact_check'; +import { + importRulesSchema, + ImportRulesSchema, + ImportRulesSchemaDecoded, +} from '../../../../common/detection_engine/schemas/request/import_rules_schema'; import { createSplitStream, createMapStream, createConcatStream, } from '../../../../../../../src/legacy/utils/streams'; -import { importRulesSchema } from '../routes/schemas/import_rules_schema'; import { BadRequestError } from '../errors/bad_request_error'; import { parseNdjsonStrings, @@ -19,14 +27,22 @@ import { } from '../../../utils/read_stream/create_stream_from_ndjson'; export const validateRules = (): Transform => { - return createMapStream((obj: ImportRuleAlertRest) => { + return createMapStream((obj: ImportRulesSchema) => { if (!(obj instanceof Error)) { - const validated = importRulesSchema.validate(obj); - if (validated.error != null) { - return new BadRequestError(validated.error.message); - } else { - return validated.value; - } + const decoded = importRulesSchema.decode(obj); + const checked = exactCheck(obj, decoded); + const onLeft = (errors: t.Errors): BadRequestError | ImportRulesSchemaDecoded => { + return new BadRequestError(errors.join()); + }; + const onRight = (schema: ImportRulesSchema): BadRequestError | ImportRulesSchemaDecoded => { + const validationErrors = importRuleValidateTypeDependents(schema); + if (validationErrors.length) { + return new BadRequestError(validationErrors.join()); + } else { + return schema as ImportRulesSchemaDecoded; + } + }; + return pipe(checked, fold(onLeft, onRight)); } else { return obj; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index f96a9e38d6a6c..ddc1b509eaf02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -49,7 +49,7 @@ describe('deleteRules', () => { expect(result).toEqual({ id: notificationId }); }); - it('should call alertsClient.delete if ruleId was null', async () => { + it('should call alertsClient.delete if ruleId was undefined', async () => { (readRules as jest.Mock).mockResolvedValue({ id: null, }); @@ -57,7 +57,7 @@ describe('deleteRules', () => { const result = await deleteRules({ alertsClient, id: notificationId, - ruleId: null, + ruleId: undefined, }); expect(alertsClient.delete).toHaveBeenCalledWith( @@ -68,7 +68,7 @@ describe('deleteRules', () => { expect(result).toEqual({ id: null }); }); - it('should return null if alertsClient.delete rejects with 404 if ruleId was null', async () => { + it('should return null if alertsClient.delete rejects with 404 if ruleId was undefined', async () => { (readRules as jest.Mock).mockResolvedValue({ id: null, }); @@ -82,7 +82,7 @@ describe('deleteRules', () => { const result = await deleteRules({ alertsClient, id: notificationId, - ruleId: null, + ruleId: undefined, }); expect(alertsClient.delete).toHaveBeenCalledWith( @@ -93,7 +93,7 @@ describe('deleteRules', () => { expect(result).toEqual(null); }); - it('should return error object if alertsClient.delete rejects with status different than 404 and if ruleId was null', async () => { + it('should return error object if alertsClient.delete rejects with status different than 404 and if ruleId was undefined', async () => { (readRules as jest.Mock).mockResolvedValue({ id: null, }); @@ -111,7 +111,7 @@ describe('deleteRules', () => { await deleteRules({ alertsClient, id: notificationId, - ruleId: null, + ruleId: undefined, }); } catch (error) { errorResult = error; @@ -125,7 +125,7 @@ describe('deleteRules', () => { expect(errorResult).toEqual(errorObject); }); - it('should return null if ruleId and id was null', async () => { + it('should return null if ruleId and id was undefined', async () => { (readRules as jest.Mock).mockResolvedValue({ id: null, }); @@ -133,7 +133,7 @@ describe('deleteRules', () => { const result = await deleteRules({ alertsClient, id: undefined, - ruleId: null, + ruleId: undefined, }); expect(result).toEqual(null); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts index 92a80bc50afc6..cde95a4967d86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts @@ -5,9 +5,9 @@ */ import { readRules } from './read_rules'; -import { DeleteRuleParams } from './types'; +import { DeleteRuleOptions } from './types'; -export const deleteRules = async ({ alertsClient, id, ruleId }: DeleteRuleParams) => { +export const deleteRules = async ({ alertsClient, id, ruleId }: DeleteRuleOptions) => { const rule = await readRules({ alertsClient, id, ruleId }); if (rule == null) { return null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts index c634f07387825..18b851c440e20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts @@ -6,7 +6,7 @@ import { FindResult } from '../../../../../alerts/server'; import { SIGNALS_ID } from '../../../../common/constants'; -import { FindRuleParams } from './types'; +import { FindRuleOptions } from './types'; export const getFilter = (filter: string | null | undefined) => { if (filter == null) { @@ -24,7 +24,7 @@ export const findRules = async ({ filter, sortField, sortOrder, -}: FindRuleParams): Promise => { +}: FindRuleOptions): Promise => { return alertsClient.find({ options: { fields, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index a3119131a0037..b7ff1cb698e17 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -34,6 +34,7 @@ export const getRulesCount = async ({ page: 1, sortField: 'createdAt', sortOrder: 'desc', + fields: undefined, }); return firstRule.total; }; @@ -53,6 +54,7 @@ export const getRules = async ({ page: 1, sortField: 'createdAt', sortOrder: 'desc', + fields: undefined, }); if (isAlertTypes(rules.data)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 38cf8008f65c8..1a965842348ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -51,7 +51,7 @@ export const getRulesFromObjects = async ( objects.reduce>>((accumPromise, object) => { const exportWorkerPromise = new Promise(async (resolve) => { try { - const rule = await readRules({ alertsClient, ruleId: object.rule_id }); + const rule = await readRules({ alertsClient, ruleId: object.rule_id, id: undefined }); if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { const transformedRule = transformAlertToRule(rule); resolve({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts index b2e1ed794043a..597a74f6efbbd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts @@ -5,8 +5,8 @@ */ import { getPrepackagedRules } from './get_prepackaged_rules'; -import { PrepackagedRules } from '../types'; import { isEmpty } from 'lodash/fp'; +import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; describe('get_existing_prepackaged_rules', () => { test('should not throw any errors with the existing checked in pre-packaged rules', () => { @@ -14,9 +14,9 @@ describe('get_existing_prepackaged_rules', () => { }); test('no rule should have the same rule_id as another rule_id', () => { - const prePacakgedRules = getPrepackagedRules(); - let existingRuleIds: PrepackagedRules[] = []; - prePacakgedRules.forEach((rule) => { + const prePackagedRules = getPrepackagedRules(); + let existingRuleIds: AddPrepackagedRulesSchemaDecoded[] = []; + prePackagedRules.forEach((rule) => { const foundDuplicate = existingRuleIds.reduce((accum, existingRule) => { if (existingRule.rule_id === rule.rule_id) { return `Found duplicate rule_id of ${rule.rule_id} between these two rule names of "${rule.name}" and "${existingRule.name}"`; @@ -33,14 +33,16 @@ describe('get_existing_prepackaged_rules', () => { }); test('should throw an exception if a pre-packaged rule is not valid', () => { + // TODO: Improve the error formatter around [object Object] expect(() => getPrepackagedRules([{ not_valid_made_up_key: true }])).toThrow( - 'name: "(rule name unknown)", rule_id: "(rule rule_id unknown)" within the folder rules/prepackaged_rules is not a valid detection engine rule. Expect the system to not work with pre-packaged rules until this rule is fixed or the file is removed. Error is: child "description" fails because ["description" is required], Full rule contents are:\n{\n "not_valid_made_up_key": true\n}' + 'name: "(rule name unknown)", rule_id: "(rule rule_id unknown)" within the folder rules/prepackaged_rules is not a valid detection engine rule. Expect the system to not work with pre-packaged rules until this rule is fixed or the file is removed. Error is: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object], Full rule contents are:\n{\n "not_valid_made_up_key": true\n}' ); }); test('should throw an exception with a message having rule_id and name in it', () => { + // TODO: Improve the error formatter around [object Object] expect(() => getPrepackagedRules([{ name: 'rule name', rule_id: 'id-123' }])).toThrow( - 'name: "rule name", rule_id: "id-123" within the folder rules/prepackaged_rules is not a valid detection engine rule. Expect the system to not work with pre-packaged rules until this rule is fixed or the file is removed. Error is: child "description" fails because ["description" is required]' + 'name: "rule name", rule_id: "id-123" within the folder rules/prepackaged_rules is not a valid detection engine rule. Expect the system to not work with pre-packaged rules until this rule is fixed or the file is removed. Error is: [object Object],[object Object],[object Object],[object Object],[object Object], Full rule contents are:\n{\n "name": "rule name",\n "rule_id": "id-123"\n}' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts index d150db1875065..d2af93c329636 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -4,8 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PrepackagedRules } from '../types'; -import { addPrepackagedRulesSchema } from '../routes/schemas/add_prepackaged_rules_schema'; +import * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck } from '../../../../common/exact_check'; +import { + addPrepackagedRulesSchema, + AddPrepackagedRulesSchema, + AddPrepackagedRulesSchemaDecoded, +} from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { BadRequestError } from '../errors/bad_request_error'; import { rawRules } from './prepackaged_rules'; @@ -14,26 +21,34 @@ import { rawRules } from './prepackaged_rules'; * that they are adding incorrect schema rules. Also this will auto-flush in all the default * aspects such as default interval of 5 minutes, default arrays, etc... */ -export const validateAllPrepackagedRules = (rules: PrepackagedRules[]): PrepackagedRules[] => { +export const validateAllPrepackagedRules = ( + rules: AddPrepackagedRulesSchema[] +): AddPrepackagedRulesSchemaDecoded[] => { return rules.map((rule) => { - const validatedRule = addPrepackagedRulesSchema.validate(rule); - if (validatedRule.error != null) { + const decoded = addPrepackagedRulesSchema.decode(rule); + const checked = exactCheck(rule, decoded); + + const onLeft = (errors: t.Errors): AddPrepackagedRulesSchemaDecoded => { const ruleName = rule.name ? rule.name : '(rule name unknown)'; const ruleId = rule.rule_id ? rule.rule_id : '(rule rule_id unknown)'; throw new BadRequestError( `name: "${ruleName}", rule_id: "${ruleId}" within the folder rules/prepackaged_rules ` + `is not a valid detection engine rule. Expect the system ` + `to not work with pre-packaged rules until this rule is fixed ` + - `or the file is removed. Error is: ${ - validatedRule.error.message - }, Full rule contents are:\n${JSON.stringify(rule, null, 2)}` + `or the file is removed. Error is: ${errors.join()}, Full rule contents are:\n${JSON.stringify( + rule, + null, + 2 + )}` ); - } else { - return validatedRule.value; - } + }; + + const onRight = (schema: AddPrepackagedRulesSchema): AddPrepackagedRulesSchemaDecoded => { + return schema as AddPrepackagedRulesSchemaDecoded; + }; + return pipe(checked, fold(onLeft, onRight)); }); }; -export const getPrepackagedRules = (rules = rawRules): PrepackagedRules[] => { - return validateAllPrepackagedRules(rules); -}; +export const getPrepackagedRules = (rules = rawRules): AddPrepackagedRulesSchemaDecoded[] => + validateAllPrepackagedRules(rules); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.test.ts index ee76bf2ef15b8..04e3ecc4e0ac0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.test.ts @@ -5,7 +5,8 @@ */ import { getRulesToInstall } from './get_rules_to_install'; -import { getResult, mockPrepackagedRule } from '../routes/__mocks__/request_responses'; +import { getResult } from '../routes/__mocks__/request_responses'; +import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; describe('get_rules_to_install', () => { test('should return empty array if both rule sets are empty', () => { @@ -14,7 +15,7 @@ describe('get_rules_to_install', () => { }); test('should return empty array if the two rule ids match', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; const installedRule = getResult(); @@ -24,7 +25,7 @@ describe('get_rules_to_install', () => { }); test('should return the rule to install if the id of the two rules do not match', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; const installedRule = getResult(); @@ -34,10 +35,10 @@ describe('get_rules_to_install', () => { }); test('should return two rules to install if both the ids of the two rules do not match', () => { - const ruleFromFileSystem1 = mockPrepackagedRule(); + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem1.rule_id = 'rule-1'; - const ruleFromFileSystem2 = mockPrepackagedRule(); + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem2.rule_id = 'rule-2'; const installedRule = getResult(); @@ -47,13 +48,13 @@ describe('get_rules_to_install', () => { }); test('should return two rules of three to install if both the ids of the two rules do not match but the third does', () => { - const ruleFromFileSystem1 = mockPrepackagedRule(); + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem1.rule_id = 'rule-1'; - const ruleFromFileSystem2 = mockPrepackagedRule(); + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem2.rule_id = 'rule-2'; - const ruleFromFileSystem3 = mockPrepackagedRule(); + const ruleFromFileSystem3 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem3.rule_id = 'rule-3'; const installedRule = getResult(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts index 4b56bc104b817..69e0255e9b55b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PrepackagedRules } from '../types'; +import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RuleAlertType } from './types'; export const getRulesToInstall = ( - rulesFromFileSystem: PrepackagedRules[], + rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], installedRules: RuleAlertType[] -): PrepackagedRules[] => { +): AddPrepackagedRulesSchemaDecoded[] => { return rulesFromFileSystem.filter( (rule) => !installedRules.some((installedRule) => installedRule.params.ruleId === rule.rule_id) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts index 40e303bddac1a..5b2eb24bcdcd2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts @@ -5,7 +5,8 @@ */ import { getRulesToUpdate } from './get_rules_to_update'; -import { getResult, mockPrepackagedRule } from '../routes/__mocks__/request_responses'; +import { getResult } from '../routes/__mocks__/request_responses'; +import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; describe('get_rules_to_update', () => { test('should return empty array if both rule sets are empty', () => { @@ -14,7 +15,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of the two rules do not match', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -26,7 +27,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of file system rule is less than the installed version', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 1; @@ -38,7 +39,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of file system rule is the same as the installed version', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 1; @@ -50,7 +51,7 @@ describe('get_rules_to_update', () => { }); test('should return the rule to update if the id of file system rule is greater than the installed version', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -62,7 +63,7 @@ describe('get_rules_to_update', () => { }); test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => { - const ruleFromFileSystem = mockPrepackagedRule(); + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -79,11 +80,11 @@ describe('get_rules_to_update', () => { }); test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => { - const ruleFromFileSystem1 = mockPrepackagedRule(); + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem1.rule_id = 'rule-1'; ruleFromFileSystem1.version = 2; - const ruleFromFileSystem2 = mockPrepackagedRule(); + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); ruleFromFileSystem2.rule_id = 'rule-2'; ruleFromFileSystem2.version = 2; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts index 4deb01eff8b63..577ad44789bdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PrepackagedRules } from '../types'; +import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RuleAlertType } from './types'; export const getRulesToUpdate = ( - rulesFromFileSystem: PrepackagedRules[], + rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], installedRules: RuleAlertType[] -): PrepackagedRules[] => { +): AddPrepackagedRulesSchemaDecoded[] => { return rulesFromFileSystem.filter((rule) => installedRules.some((installedRule) => { return ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index 7b2cef9060f8c..a51acf99b570c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { Alert } from '../../../../../alerts/common'; import { AlertsClient } from '../../../../../alerts/server'; import { createRules } from './create_rules'; -import { PrepackagedRules } from '../types'; +import { PartialFilter } from '../types'; export const installPrepackagedRules = ( alertsClient: AlertsClient, - rules: PrepackagedRules[], + rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string ): Array> => rules.reduce>>((acc, rule) => { @@ -21,7 +22,6 @@ export const installPrepackagedRules = ( enabled, false_positives: falsePositives, from, - immutable, query, language, machine_learning_job_id: machineLearningJobId, @@ -29,7 +29,7 @@ export const installPrepackagedRules = ( timeline_id: timelineId, timeline_title: timelineTitle, meta, - filters, + filters: filtersObject, rule_id: ruleId, index, interval, @@ -44,8 +44,11 @@ export const installPrepackagedRules = ( references, note, version, - exceptions_list, + exceptions_list: exceptionsList, } = rule; + // TODO: Fix these either with an is conversion or by better typing them within io-ts + const filters: PartialFilter[] | undefined = filtersObject as PartialFilter[]; + return [ ...acc, createRules({ @@ -55,7 +58,7 @@ export const installPrepackagedRules = ( enabled, falsePositives, from, - immutable, + immutable: true, // At the moment we force all prepackaged rules to be immutable query, language, machineLearningJobId, @@ -79,7 +82,7 @@ export const installPrepackagedRules = ( references, note, version, - exceptions_list, + exceptionsList, actions: [], // At this time there is no pre-packaged actions }), ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts new file mode 100644 index 0000000000000..e711d8d2ac287 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -0,0 +1,183 @@ +/* + * 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 { PatchRulesOptions } from './types'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; +import { SanitizedAlert } from '../../../../../alerts/common'; + +const rule: SanitizedAlert = { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + name: 'Detect Root/Admin Users', + tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`], + alertTypeId: 'siem.signals', + consumer: 'siem', + params: { + anomalyThreshold: undefined, + description: 'Detecting root and admin users', + ruleId: 'rule-1', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + falsePositives: [], + from: 'now-6m', + immutable: false, + query: 'user.name: root or user.name: admin', + language: 'kuery', + machineLearningJobId: undefined, + outputIndex: '.siem-signals', + timelineId: 'some-timeline-id', + timelineTitle: 'some-timeline-title', + meta: { someMeta: 'someField' }, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + riskScore: 50, + maxSignals: 100, + severity: 'high', + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + references: ['http://www.example.com', 'https://ww.example.com'], + note: '# Investigative notes', + version: 1, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'exists', + }, + { + field: 'host.name', + values_operator: 'excluded', + values_type: 'match', + values: [ + { + name: 'rock01', + }, + ], + and: [ + { + field: 'host.id', + values_operator: 'included', + values_type: 'match_all', + values: [ + { + name: '123', + }, + { + name: '678', + }, + ], + }, + ], + }, + ], + }, + createdAt: new Date('2019-12-13T16:40:33.400Z'), + updatedAt: new Date('2019-12-13T16:40:33.400Z'), + schedule: { interval: '5m' }, + enabled: true, + actions: [], + throttle: null, + createdBy: 'elastic', + updatedBy: 'elastic', + apiKeyOwner: 'elastic', + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '2dabe330-0702-11ea-8b50-773b89126888', +}; + +export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ + alertsClient: alertsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), + anomalyThreshold: undefined, + description: 'some description', + enabled: true, + falsePositives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + query: 'user.name: root or user.name: admin', + language: 'kuery', + savedId: 'savedId-123', + timelineId: 'timelineid-123', + timelineTitle: 'timeline-title-123', + meta: {}, + machineLearningJobId: undefined, + filters: [], + index: ['index-123'], + interval: '5m', + maxSignals: 100, + riskScore: 80, + outputIndex: 'output-1', + name: 'Query with a rule id', + severity: 'high', + tags: [], + threat: [], + to: 'now', + type: 'query', + references: ['http://www.example.com'], + note: '# sample markdown', + version: 1, + exceptionsList: [], + actions: [], + rule, +}); + +export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ + alertsClient: alertsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), + anomalyThreshold: 55, + description: 'some description', + enabled: true, + falsePositives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + query: undefined, + language: undefined, + savedId: 'savedId-123', + timelineId: 'timelineid-123', + timelineTitle: 'timeline-title-123', + meta: {}, + machineLearningJobId: 'new_job_id', + filters: [], + index: ['index-123'], + interval: '5m', + maxSignals: 100, + riskScore: 80, + outputIndex: 'output-1', + name: 'Machine Learning Job', + severity: 'high', + tags: [], + threat: [], + to: 'now', + type: 'machine_learning', + references: ['http://www.example.com'], + note: '# sample markdown', + version: 1, + exceptionsList: [], + actions: [], + rule, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts index 0dffa665f780b..1ca10d9067bd9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.test.ts @@ -4,83 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerts/server/mocks'; -import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { patchRules } from './patch_rules'; +import { getPatchRulesOptionsMock, getPatchMlRulesOptionsMock } from './patch_rules.mock'; +import { PatchRulesOptions } from './types'; describe('patchRules', () => { - let alertsClient: ReturnType; - let savedObjectsClient: ReturnType; - - beforeEach(() => { - alertsClient = alertsClientMock.create(); - savedObjectsClient = savedObjectsClientMock.create(); - }); - - it('should call alertsClient.disable is the rule was enabled and enabled is false', async () => { - const existingRule = getResult(); - const params = getResult().params; - - await patchRules({ - alertsClient, - savedObjectsClient, - rule: existingRule, - ...params, + it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => { + const rulesOptionsMock = getPatchRulesOptionsMock(); + const ruleOptions: PatchRulesOptions = { + ...rulesOptionsMock, enabled: false, - interval: '', - name: '', - tags: [], - }); - - expect(alertsClient.disable).toHaveBeenCalledWith( + }; + await patchRules(ruleOptions); + expect(ruleOptions.alertsClient.disable).toHaveBeenCalledWith( expect.objectContaining({ - id: existingRule.id, + id: ruleOptions.rule?.id, }) ); }); - it('should call alertsClient.enable is the rule was disabled and enabled is true', async () => { - const existingRule = { - ...getResult(), - enabled: false, - }; - const params = getResult().params; - - await patchRules({ - alertsClient, - savedObjectsClient, - rule: existingRule, - ...params, + it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => { + const rulesOptionsMock = getPatchRulesOptionsMock(); + const ruleOptions: PatchRulesOptions = { + ...rulesOptionsMock, enabled: true, - interval: '', - name: '', - tags: [], - }); - - expect(alertsClient.enable).toHaveBeenCalledWith( + }; + if (ruleOptions.rule != null) { + ruleOptions.rule.enabled = false; + } + await patchRules(ruleOptions); + expect(ruleOptions.alertsClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - id: existingRule.id, + id: ruleOptions.rule?.id, }) ); }); it('calls the alertsClient with ML params', async () => { - const existingRule = getMlResult(); - const params = { - ...getMlResult().params, - anomalyThreshold: 55, - machineLearningJobId: 'new_job_id', + const rulesOptionsMock = getPatchMlRulesOptionsMock(); + const ruleOptions: PatchRulesOptions = { + ...rulesOptionsMock, + enabled: true, }; - - await patchRules({ - alertsClient, - savedObjectsClient, - rule: existingRule, - ...params, - }); - - expect(alertsClient.update).toHaveBeenCalledWith( + if (ruleOptions.rule != null) { + ruleOptions.rule.enabled = false; + } + await patchRules(ruleOptions); + expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ @@ -94,25 +64,22 @@ describe('patchRules', () => { describe('regression tests', () => { it("updates the rule's actions if provided", async () => { - const existingRule = getResult(); - - const action = { - action_type_id: '.slack', - id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', - params: { - message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', - }, - group: 'default', + const rulesOptionsMock = getPatchRulesOptionsMock(); + const ruleOptions: PatchRulesOptions = { + ...rulesOptionsMock, + actions: [ + { + action_type_id: '.slack', + id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', + }, + group: 'default', + }, + ], }; - - await patchRules({ - alertsClient, - savedObjectsClient, - actions: [action], - rule: existingRule, - }); - - expect(alertsClient.update).toHaveBeenCalledWith( + await patchRules(ruleOptions); + expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ actions: [ @@ -131,9 +98,10 @@ describe('patchRules', () => { }); it('does not update actions if none are specified', async () => { - const existingRule = { - ...getResult(), - actions: [ + const ruleOptions = getPatchRulesOptionsMock(); + delete ruleOptions.actions; + if (ruleOptions.rule != null) { + ruleOptions.rule.actions = [ { actionTypeId: '.slack', id: '2933e581-d81c-4fe3-88fe-c57c6b8a5bfd', @@ -142,16 +110,11 @@ describe('patchRules', () => { }, group: 'default', }, - ], - }; - - await patchRules({ - alertsClient, - savedObjectsClient, - rule: existingRule, - }); + ]; + } - expect(alertsClient.update).toHaveBeenCalledWith( + await patchRules(ruleOptions); + expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ actions: [ 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 950a3e90fb70c..3796df353abb8 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 @@ -7,10 +7,11 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerts/server'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { PatchRuleParams } from './types'; +import { PatchRulesOptions } from './types'; import { addTags } from './add_tags'; import { calculateVersion, calculateName, calculateInterval } from './utils'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; +import { Meta } from '../types'; export const patchRules = async ({ alertsClient, @@ -27,7 +28,6 @@ export const patchRules = async ({ meta, filters, from, - immutable, index, interval, maxSignals, @@ -42,11 +42,11 @@ export const patchRules = async ({ references, note, version, - exceptions_list, + exceptionsList, anomalyThreshold, machineLearningJobId, actions, -}: PatchRuleParams): Promise => { +}: PatchRulesOptions): Promise => { if (rule == null) { return null; } @@ -60,7 +60,7 @@ export const patchRules = async ({ savedId, timelineId, timelineTitle, - meta, + meta: meta as Meta | undefined, // TODO: Remove this cast once we fix the types for calculate version and patch, filters, from, index, @@ -76,7 +76,7 @@ export const patchRules = async ({ references, version, note, - exceptions_list, + exceptionsList, anomalyThreshold, machineLearningJobId, }); @@ -89,7 +89,6 @@ export const patchRules = async ({ description, falsePositives, from, - immutable, query, language, outputIndex, @@ -108,7 +107,7 @@ export const patchRules = async ({ references, note, version: calculatedVersion, - exceptions_list, + exceptionsList, anomalyThreshold, machineLearningJobId, } @@ -117,7 +116,7 @@ export const patchRules = async ({ const update = await alertsClient.update({ id: rule.id, data: { - tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), + tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), throttle: null, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts index ef8e70c78422c..b17a7358cfa52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts @@ -81,18 +81,6 @@ describe('read_rules', () => { } }); - test('should return the output from alertsClient if id is set but ruleId is null', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - - const rule = await readRules({ - alertsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ruleId: null, - }); - expect(rule).toEqual(getResult()); - }); - test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -126,25 +114,12 @@ describe('read_rules', () => { const rule = await readRules({ alertsClient, - id: null, + id: undefined, ruleId: 'rule-1', }); expect(rule).toEqual(getResult()); }); - test('should return null if id and ruleId are null', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const rule = await readRules({ - alertsClient, - id: null, - ruleId: null, - }); - expect(rule).toEqual(null); - }); - test('should return null if id and ruleId are undefined', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts index a8b76aeb8c453..e4bb65a907e2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts @@ -7,7 +7,7 @@ import { SanitizedAlert } from '../../../../../alerts/common'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; -import { ReadRuleParams, isAlertType } from './types'; +import { isAlertType, ReadRuleOptions } from './types'; /** * This reads the rules through a cascade try of what is fastest to what is slowest. @@ -21,7 +21,7 @@ export const readRules = async ({ alertsClient, id, ruleId, -}: ReadRuleParams): Promise => { +}: ReadRuleOptions): Promise => { if (id != null) { try { const rule = await alertsClient.get({ id }); @@ -43,6 +43,10 @@ export const readRules = async ({ alertsClient, filter: `alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"`, page: 1, + fields: undefined, + perPage: undefined, + sortField: undefined, + sortOrder: undefined, }); if (ruleFromFind.data.length === 0 || !isAlertType(ruleFromFind.data[0])) { return null; 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 70d53090f81cc..8bdb1287c6df2 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,29 +13,67 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { ListsDefaultArraySchema } from '../../../../common/detection_engine/schemas/types/lists_default_array'; +import { + FalsePositives, + From, + RuleId, + Immutable, + DescriptionOrUndefined, + Interval, + MaxSignals, + RiskScore, + OutputIndex, + Name, + Severity, + Tags, + Threat, + To, + Type, + References, + Version, + AnomalyThresholdOrUndefined, + QueryOrUndefined, + LanguageOrUndefined, + SavedIdOrUndefined, + TimelineIdOrUndefined, + TimelineTitleOrUndefined, + MachineLearningJobIdOrUndefined, + IndexOrUndefined, + NoteOrUndefined, + MetaOrUndefined, + Description, + Enabled, + VersionOrUndefined, + IdOrUndefined, + RuleIdOrUndefined, + EnabledOrUndefined, + FalsePositivesOrUndefined, + FromOrUndefined, + OutputIndexOrUndefined, + IntervalOrUndefined, + MaxSignalsOrUndefined, + RiskScoreOrUndefined, + NameOrUndefined, + SeverityOrUndefined, + TagsOrUndefined, + ToOrUndefined, + ThreatOrUndefined, + TypeOrUndefined, + ReferencesOrUndefined, + ListAndOrUndefined, + PerPageOrUndefined, + PageOrUndefined, + SortFieldOrUndefined, + QueryFilterOrUndefined, + FieldsOrUndefined, + SortOrderOrUndefined, +} from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; -import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; - -export type PatchRuleAlertParamsRest = Partial & { - id: string | undefined; - rule_id: RuleAlertParams['ruleId'] | undefined; -}; - -export type UpdateRuleAlertParamsRest = RuleAlertParamsRest & { - id: string | undefined; - rule_id: RuleAlertParams['ruleId'] | undefined; -}; - -export interface FindParamsRest { - per_page: number; - page: number; - sort_field: string; - sort_order: 'asc' | 'desc'; - fields: string[]; - filter: string; -} +import { RuleAlertParams, RuleTypeParams, PartialFilter } from '../types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -90,81 +128,17 @@ export interface HapiReadableStream extends Readable { filename: string; }; } -export interface ImportRulesRequestParams { - query: { overwrite: boolean }; - body: { file: HapiReadableStream }; -} - -export interface ExportRulesRequestParams { - body: { objects: Array<{ rule_id: string }> | null | undefined }; - query: { - file_name: string; - exclude_export_details: boolean; - }; -} - -export interface RuleRequestParams { - id: string | undefined; - rule_id: string | undefined; -} - -export type ReadRuleRequestParams = RuleRequestParams; -export type DeleteRuleRequestParams = RuleRequestParams; -export type DeleteRulesRequestParams = RuleRequestParams[]; - -export interface FindRuleParams { - alertsClient: AlertsClient; - perPage?: number; - page?: number; - sortField?: string; - filter?: string; - fields?: string[]; - sortOrder?: 'asc' | 'desc'; -} - -export interface FindRulesRequestParams { - per_page: number; - page: number; - search?: string; - sort_field?: string; - filter?: string; - fields?: string[]; - sort_order?: 'asc' | 'desc'; -} - -export interface FindRulesStatusesRequestParams { - ids: string[]; -} export interface Clients { alertsClient: AlertsClient; } +// TODO: Try and remove this patch export type PatchRuleParams = Partial> & { rule: SanitizedAlert | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; -export type UpdateRuleParams = Omit & { - id: string | undefined | null; - savedObjectsClient: SavedObjectsClientContract; -} & Clients; - -export type DeleteRuleParams = Clients & { - id: string | undefined; - ruleId: string | undefined | null; -}; - -export type CreateRuleParams = Omit & { - ruleId: string; -} & Clients; - -export interface ReadRuleParams { - alertsClient: AlertsClient; - id?: string | undefined | null; - ruleId?: string | undefined | null; -} - export const isAlertTypes = (partialAlert: PartialAlert[]): partialAlert is RuleAlertType[] => { return partialAlert.every((rule) => isAlertType(rule)); }; @@ -190,3 +164,131 @@ export const isRuleStatusFindTypes = ( ): obj is Array> => { return obj ? obj.every((ruleStatus) => isRuleStatusFindType(ruleStatus)) : false; }; + +export interface CreateRulesOptions { + alertsClient: AlertsClient; + anomalyThreshold: AnomalyThresholdOrUndefined; + description: Description; + enabled: Enabled; + falsePositives: FalsePositives; + from: From; + query: QueryOrUndefined; + language: LanguageOrUndefined; + savedId: SavedIdOrUndefined; + timelineId: TimelineIdOrUndefined; + timelineTitle: TimelineTitleOrUndefined; + meta: MetaOrUndefined; + machineLearningJobId: MachineLearningJobIdOrUndefined; + filters: PartialFilter[]; + ruleId: RuleId; + immutable: Immutable; + index: IndexOrUndefined; + interval: Interval; + maxSignals: MaxSignals; + riskScore: RiskScore; + outputIndex: OutputIndex; + name: Name; + severity: Severity; + tags: Tags; + threat: Threat; + to: To; + type: Type; + references: References; + note: NoteOrUndefined; + version: Version; + exceptionsList: ListsDefaultArraySchema; + actions: RuleAlertAction[]; +} + +export interface UpdateRulesOptions { + id: IdOrUndefined; + savedObjectsClient: SavedObjectsClientContract; + alertsClient: AlertsClient; + anomalyThreshold: AnomalyThresholdOrUndefined; + description: Description; + enabled: Enabled; + 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; + maxSignals: MaxSignals; + riskScore: RiskScore; + outputIndex: OutputIndex; + name: Name; + severity: Severity; + tags: Tags; + threat: Threat; + to: To; + type: Type; + references: References; + note: NoteOrUndefined; + version: VersionOrUndefined; + exceptionsList: ListsDefaultArraySchema; + actions: RuleAlertAction[]; +} + +export interface PatchRulesOptions { + savedObjectsClient: SavedObjectsClientContract; + alertsClient: AlertsClient; + anomalyThreshold: AnomalyThresholdOrUndefined; + description: DescriptionOrUndefined; + enabled: EnabledOrUndefined; + falsePositives: FalsePositivesOrUndefined; + from: FromOrUndefined; + query: QueryOrUndefined; + language: LanguageOrUndefined; + savedId: SavedIdOrUndefined; + timelineId: TimelineIdOrUndefined; + timelineTitle: TimelineTitleOrUndefined; + meta: MetaOrUndefined; + machineLearningJobId: MachineLearningJobIdOrUndefined; + filters: PartialFilter[]; + index: IndexOrUndefined; + interval: IntervalOrUndefined; + maxSignals: MaxSignalsOrUndefined; + riskScore: RiskScoreOrUndefined; + outputIndex: OutputIndexOrUndefined; + name: NameOrUndefined; + severity: SeverityOrUndefined; + tags: TagsOrUndefined; + threat: ThreatOrUndefined; + to: ToOrUndefined; + type: TypeOrUndefined; + references: ReferencesOrUndefined; + note: NoteOrUndefined; + version: VersionOrUndefined; + exceptionsList: ListAndOrUndefined; + actions: RuleAlertAction[] | undefined; + rule: SanitizedAlert | null; +} + +export interface ReadRuleOptions { + alertsClient: AlertsClient; + id: IdOrUndefined; + ruleId: RuleIdOrUndefined; +} + +export interface DeleteRuleOptions { + alertsClient: AlertsClient; + id: IdOrUndefined; + ruleId: RuleIdOrUndefined; +} + +export interface FindRuleOptions { + alertsClient: AlertsClient; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + filter: QueryFilterOrUndefined; + fields: FieldsOrUndefined; + sortOrder: SortOrderOrUndefined; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index ede5c51d1e5e7..1945d00c75c88 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -6,12 +6,10 @@ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { alertsClientMock } from '../../../../../alerts/server/mocks'; -import { - mockPrepackagedRule, - getFindResultWithSingleHit, -} from '../routes/__mocks__/request_responses'; +import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; +import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; jest.mock('./patch_rules'); describe('updatePrepackagedRules', () => { @@ -33,7 +31,7 @@ describe('updatePrepackagedRules', () => { }, ]; const outputIndex = 'outputIndex'; - const prepackagedRule = mockPrepackagedRule(); + const prepackagedRule = getAddPrepackagedRulesSchemaDecodedMock(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); await updatePrepackagedRules( @@ -44,9 +42,14 @@ describe('updatePrepackagedRules', () => { ); expect(patchRules).toHaveBeenCalledWith( - expect.not.objectContaining({ - enabled: true, - actions, + expect.objectContaining({ + actions: undefined, + }) + ); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + enabled: undefined, }) ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index c793d7eb9b6dc..c4792eaa97ee1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -5,15 +5,16 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { AlertsClient } from '../../../../../alerts/server'; import { patchRules } from './patch_rules'; -import { PrepackagedRules } from '../types'; import { readRules } from './read_rules'; +import { PartialFilter } from '../types'; export const updatePrepackagedRules = async ( alertsClient: AlertsClient, savedObjectsClient: SavedObjectsClientContract, - rules: PrepackagedRules[], + rules: AddPrepackagedRulesSchemaDecoded[], outputIndex: string ): Promise => { await Promise.all( @@ -22,12 +23,11 @@ export const updatePrepackagedRules = async ( description, false_positives: falsePositives, from, - immutable, query, language, saved_id: savedId, meta, - filters, + filters: filtersObject, rule_id: ruleId, index, interval, @@ -42,10 +42,18 @@ export const updatePrepackagedRules = async ( references, version, note, + anomaly_threshold: anomalyThreshold, + timeline_id: timelineId, + timeline_title: timelineTitle, + machine_learning_job_id: machineLearningJobId, + exceptions_list: exceptionsList, } = rule; const existingRule = await readRules({ alertsClient, ruleId, id: undefined }); + // TODO: Fix these either with an is conversion or by better typing them within io-ts + const filters: PartialFilter[] | undefined = filtersObject as PartialFilter[]; + // Note: we do not pass down enabled as we do not want to suddenly disable // or enable rules on the user when they were not expecting it if a rule updates return patchRules({ @@ -53,7 +61,6 @@ export const updatePrepackagedRules = async ( description, falsePositives, from, - immutable, query, language, outputIndex, @@ -75,6 +82,13 @@ export const updatePrepackagedRules = async ( references, version, note, + anomalyThreshold, + enabled: undefined, + timelineId, + timelineTitle, + machineLearningJobId, + exceptionsList, + actions: undefined, }); }) ); 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 new file mode 100644 index 0000000000000..7812c66a74d1f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -0,0 +1,81 @@ +/* + * 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 { UpdateRulesOptions } from './types'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; + +export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + alertsClient: alertsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), + anomalyThreshold: undefined, + description: 'some description', + enabled: true, + falsePositives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + query: 'user.name: root or user.name: admin', + language: 'kuery', + 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, + outputIndex: 'output-1', + name: 'Query with a rule id', + severity: 'high', + tags: [], + threat: [], + to: 'now', + type: 'query', + references: ['http://www.example.com'], + note: '# sample markdown', + version: 1, + exceptionsList: [], + actions: [], +}); + +export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + alertsClient: alertsClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), + anomalyThreshold: 55, + description: 'some description', + enabled: true, + falsePositives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + query: undefined, + language: undefined, + 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, + outputIndex: 'output-1', + name: 'Machine Learning Job', + severity: 'high', + tags: [], + threat: [], + to: 'now', + type: 'machine_learning', + references: ['http://www.example.com'], + note: '# sample markdown', + version: 1, + exceptionsList: [], + actions: [], +}); 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 222411deb37ab..cf59d2bb5e8c4 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 @@ -4,96 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; +import { getUpdateRulesOptionsMock, getUpdateMlRulesOptionsMock } from './update_rules.mock'; +import { AlertsClientMock } from '../../../../../alerts/server/alerts_client.mock'; describe('updateRules', () => { - let alertsClient: ReturnType; - let savedObjectsClient: ReturnType; - - beforeEach(() => { - alertsClient = alertsClientMock.create(); - savedObjectsClient = savedObjectsClientMock.create(); - }); - - it('should call alertsClient.disable is the rule was enabled and enabled is false', async () => { - const rule = getResult(); - alertsClient.get.mockResolvedValue(getResult()); - - await updateRules({ - alertsClient, - savedObjectsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ...rule.params, + it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => { + const rulesOptionsMock = getUpdateRulesOptionsMock(); + const ruleOptions = { + ...rulesOptionsMock, enabled: false, - interval: '', - name: '', - tags: [], - actions: [], - }); + }; + ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(getResult()); + + await updateRules(ruleOptions); - expect(alertsClient.disable).toHaveBeenCalledWith( + expect(ruleOptions.alertsClient.disable).toHaveBeenCalledWith( expect.objectContaining({ - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + id: rulesOptionsMock.id, }) ); }); - it('should call alertsClient.enable is the rule was disabled and enabled is true', async () => { - const rule = getResult(); - alertsClient.get.mockResolvedValue({ + it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => { + const rulesOptionsMock = getUpdateRulesOptionsMock(); + const ruleOptions = { + ...rulesOptionsMock, + enabled: true, + }; + + ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({ ...getResult(), enabled: false, }); - await updateRules({ - alertsClient, - savedObjectsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ...rule.params, - enabled: true, - interval: '', - name: '', - tags: [], - actions: [], - }); + await updateRules(ruleOptions); - expect(alertsClient.enable).toHaveBeenCalledWith( + expect(ruleOptions.alertsClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + id: rulesOptionsMock.id, }) ); }); it('calls the alertsClient with ML params', async () => { - alertsClient.get.mockResolvedValue(getMlResult()); - - const params = { - ...getMlResult().params, - anomalyThreshold: 55, - machineLearningJobId: 'new_job_id', + const rulesOptionsMock = getUpdateMlRulesOptionsMock(); + const ruleOptions = { + ...rulesOptionsMock, + enabled: true, }; - await updateRules({ - alertsClient, - savedObjectsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ...params, - enabled: true, - interval: '', - name: '', - tags: [], - actions: [], - }); + ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + getMlResult() + ); + + await updateRules(ruleOptions); - expect(alertsClient.update).toHaveBeenCalledWith( + expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ - anomalyThreshold: 55, - machineLearningJobId: 'new_job_id', + anomalyThreshold: rulesOptionsMock.anomalyThreshold, + machineLearningJobId: rulesOptionsMock.machineLearningJobId, }), }), }) 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 54031b6e35bf1..0236c357988d5 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 @@ -7,11 +7,12 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerts/server'; import { readRules } from './read_rules'; -import { UpdateRuleParams } from './types'; +import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; import { calculateVersion } from './utils'; import { hasListsFeature } from '../feature_flags'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; +import { Meta } from '../types'; export const updateRules = async ({ alertsClient, @@ -43,11 +44,11 @@ export const updateRules = async ({ references, version, note, - exceptions_list, + exceptionsList, anomalyThreshold, machineLearningJobId, actions, -}: UpdateRuleParams): Promise => { +}: UpdateRulesOptions): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { return null; @@ -62,7 +63,7 @@ export const updateRules = async ({ savedId, timelineId, timelineTitle, - meta, + meta: meta as Meta, // TODO: Remove this cast once we fix the types for calculate version and patch filters, from, index, @@ -83,7 +84,7 @@ export const updateRules = async ({ }); // TODO: Remove this and use regular exceptions_list once the feature is stable for a release - const exceptionsListParam = hasListsFeature() ? { exceptions_list } : {}; + const exceptionsListParam = hasListsFeature() ? { exceptionsList } : {}; const update = await alertsClient.update({ id: rule.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_ndjson_prepackaged_rules.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_ndjson_prepackaged_rules.sh new file mode 100755 index 0000000000000..29c69ae857f3e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_ndjson_prepackaged_rules.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +i=0; +for f in ../rules/prepackaged_rules/*.json ; do + ((i++)); + echo "converting $f" + cat ../rules/prepackaged_rules/windows_msxsl_network.json | tr -d '\n' | tr -d ' ' >> pre_packaged_rules.ndjson + echo "" >> pre_packaged_rules.ndjson +done +echo "{\"exported_count\":$i,\"missing_rules\":[],\"missing_rules_count\":0}" >> pre_packaged_rules.ndjson + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 2d75ba4f42d12..46a16e7dca153 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -44,7 +44,7 @@ export const sampleRuleAlertParams = ( meta: undefined, threat: undefined, version: 1, - exceptions_list: [ + exceptionsList: [ { field: 'source.ip', values_operator: 'included', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts index b33a2376589ef..d4efd9a2e8a1a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -189,7 +189,7 @@ export const buildQueryExceptions = ({ }: { query: string; language: Language; - lists: RuleAlertParams['exceptions_list']; + lists: RuleAlertParams['exceptionsList']; }): Query[] => { if (lists && lists !== null) { const exceptions = buildExceptions({ lists, language, query }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index 93d4e5e7719b2..de8de1bc513e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -72,7 +72,7 @@ export const buildRule = ({ version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, - exceptions_list: ruleParams.exceptions_list, + exceptions_list: ruleParams.exceptionsList, machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 07435fda0da2e..29b8b54d162df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -14,7 +14,7 @@ import { RuleAlertParams } from '../types'; interface FilterEventsAgainstList { listClient: ListClient; - exceptionsList: RuleAlertParams['exceptions_list']; + exceptionsList: RuleAlertParams['exceptionsList']; logger: Logger; eventSearchResult: SignalSearchResponse; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 1630192b3c03a..c464238715afd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -22,7 +22,7 @@ export const getQueryFilter = ( language: Language, filters: PartialFilter[], index: string[], - lists: RuleAlertParams['exceptions_list'] + lists: RuleAlertParams['exceptionsList'] ) => { const indexPattern = { fields: [], @@ -53,7 +53,7 @@ interface GetFilterArgs { savedId: string | undefined | null; services: AlertServices; index: string[] | undefined | null; - lists: RuleAlertParams['exceptions_list']; + lists: RuleAlertParams['exceptionsList']; } interface QueryAttributes { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index e44b82224d1ce..b7bea906475db 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -18,7 +18,7 @@ interface SearchAfterAndBulkCreateParams { ruleParams: RuleTypeParams; services: AlertServices; listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged - exceptionsList: RuleAlertParams['exceptions_list']; + exceptionsList: RuleAlertParams['exceptionsList']; logger: Logger; id: string; inputIndexPattern: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index 81a6ce9b08f02..d42ba8fe57005 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -39,5 +39,5 @@ export const signalParamsSchema = () => type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), version: schema.number({ defaultValue: 1 }), - exceptions_list: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 6885b4c814679..567274be6a9f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -79,7 +79,7 @@ export const signalRulesAlertType = ({ query, to, type, - exceptions_list: exceptionsList, + exceptionsList, } = params; const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 90497b6e34cb4..869c81f640561 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -21,20 +21,6 @@ export interface SignalsStatusParams { status: 'open' | 'closed'; } -export interface SignalQueryParams { - query: object | undefined | null; - aggs: object | undefined | null; - _source: string[] | undefined | null; - size: number | undefined | null; - track_total_hits: boolean | undefined | null; -} - -export type SignalsStatusRestParams = Omit & { - signal_ids: SignalsStatusParams['signalIds']; -}; - -export type SignalsQueryRestParams = SignalQueryParams; - export type SearchTypes = | string | string[] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts index 2bb2b5ec47e2f..5bf2b47c78181 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/tags/read_tags.ts @@ -61,6 +61,7 @@ export const readRawTags = async ({ page: 1, sortField: 'createdAt', sortOrder: 'desc', + filter: undefined, }); // Get all the rules to aggregate over all the tags of the rules const rules = await findRules({ @@ -70,6 +71,7 @@ export const readRawTags = async ({ sortField: 'createdAt', sortOrder: 'desc', page: 1, + filter: undefined, }); const tagSet = convertTagsToSet(rules.data); return Array.from(tagSet); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 53c8a9bf0a7e7..9062de49fa6ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -68,7 +68,7 @@ export interface RuleAlertParams { type: RuleType; version: number; throttle: string | undefined | null; - exceptions_list: ListsDefaultArraySchema | null | undefined; + exceptionsList: ListsDefaultArraySchema | null | undefined; } export type RuleTypeParams = Omit< @@ -83,6 +83,7 @@ export type RuleAlertParamsRest = Omit< | 'falsePositives' | 'immutable' | 'maxSignals' + | 'exceptionsList' | 'machineLearningJobId' | 'savedId' | 'riskScore' @@ -101,6 +102,7 @@ export type RuleAlertParamsRest = Omit< | 'lastFailureMessage' > & { anomaly_threshold: RuleAlertParams['anomalyThreshold']; + exceptions_list: RuleAlertParams['exceptionsList']; rule_id: RuleAlertParams['ruleId']; false_positives: RuleAlertParams['falsePositives']; saved_id?: RuleAlertParams['savedId']; @@ -127,24 +129,6 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { immutable: boolean; }; -export type ImportRuleAlertRest = Omit & { - id: string | undefined | null; - rule_id: string; - immutable: boolean; -}; - -export type PrepackagedRules = Omit< - RuleAlertParamsRest, - | 'status' - | 'status_date' - | 'last_failure_at' - | 'last_success_at' - | 'last_failure_message' - | 'last_success_message' - | 'updated_at' - | 'created_at' -> & { rule_id: string; immutable: boolean }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any export type CallWithRequest, V> = ( endpoint: string, diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts index 2b5b34edca140..ca2f881d6abd8 100644 --- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { transformDataToNdjson } from './create_stream_from_ndjson'; -import { ImportRuleAlertRest } from '../../lib/detection_engine/types'; import { sampleRule } from '../../lib/detection_engine/signals/__mocks__/es_results'; +import { ImportRulesSchemaDecoded } from '../../../common/detection_engine/schemas/request/import_rules_schema'; -export const getOutputSample = (): Partial => ({ +export const getOutputSample = (): Partial => ({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -21,7 +21,7 @@ export const getOutputSample = (): Partial => ({ type: 'query', }); -export const getSampleAsNdjson = (sample: Partial): string => { +export const getSampleAsNdjson = (sample: Partial): string => { return `${JSON.stringify(sample)}\n`; }; diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts index 7da9fbcfb7662..f455ac0696e4e 100644 --- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts @@ -5,9 +5,17 @@ */ import { Transform } from 'stream'; import { has, isString } from 'lodash/fp'; -import { ImportRuleAlertRest } from '../../lib/detection_engine/types'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; +import { importRuleValidateTypeDependents } from '../../../common/detection_engine/schemas/request/import_rules_type_dependents'; +import { + ImportRulesSchemaDecoded, + importRulesSchema, + ImportRulesSchema, +} from '../../../common/detection_engine/schemas/request/import_rules_schema'; +import { exactCheck } from '../../../common/exact_check'; import { createMapStream, createFilterStream } from '../../../../../../src/legacy/utils/streams'; -import { importRulesSchema } from '../../lib/detection_engine/routes/schemas/import_rules_schema'; import { BadRequestError } from '../../lib/detection_engine/errors/bad_request_error'; export interface RulesObjectsExportResultDetails { @@ -28,20 +36,28 @@ export const parseNdjsonStrings = (): Transform => { }; export const filterExportedCounts = (): Transform => { - return createFilterStream( + return createFilterStream( (obj) => obj != null && !has('exported_count', obj) ); }; export const validateRules = (): Transform => { - return createMapStream((obj: ImportRuleAlertRest) => { + return createMapStream((obj: ImportRulesSchema) => { if (!(obj instanceof Error)) { - const validated = importRulesSchema.validate(obj); - if (validated.error != null) { - return new BadRequestError(validated.error.message); - } else { - return validated.value; - } + const decoded = importRulesSchema.decode(obj); + const checked = exactCheck(obj, decoded); + const onLeft = (errors: t.Errors): BadRequestError | ImportRulesSchemaDecoded => { + return new BadRequestError(errors.join()); + }; + const onRight = (schema: ImportRulesSchema): BadRequestError | ImportRulesSchemaDecoded => { + const validationErrors = importRuleValidateTypeDependents(schema); + if (validationErrors.length) { + return new BadRequestError(validationErrors.join()); + } else { + return schema as ImportRulesSchemaDecoded; + } + }; + return pipe(checked, fold(onLeft, onRight)); } else { return obj; } diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts index e91b735e1629b..5626d63b8f230 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules.ts @@ -92,12 +92,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return an error if the id does not exist when trying to delete it', async () => { const { body } = await supertest - .delete(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .delete(`${DETECTION_ENGINE_RULES_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) .set('kbn-xsrf', 'true') .expect(404); expect(body).to.eql({ - message: 'id: "fake_id" not found', + message: 'id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" not found', status_code: 404, }); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts index 687551c2d8313..04f168d5b7953 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/delete_rules_bulk.ts @@ -114,17 +114,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should return an error if the id does not exist when trying to delete an id', async () => { const { body } = await supertest .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: 'fake_id' }]) + .send([{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); expect(body).to.eql([ { error: { - message: 'id: "fake_id" not found', + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', status_code: 404, }, - id: 'fake_id', + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', }, ]); }); @@ -139,14 +139,20 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .send([{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); expect([bodyToCompare, body[1]]).to.eql([ getSimpleRuleOutputWithoutRuleId(), - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', + error: { + status_code: 404, + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', + }, + }, ]); }); }); @@ -241,17 +247,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should return an error if the id does not exist when trying to delete an id', async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: 'fake_id' }]) + .send([{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); expect(body).to.eql([ { error: { - message: 'id: "fake_id" not found', + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', status_code: 404, }, - id: 'fake_id', + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', }, ]); }); @@ -266,14 +272,20 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .send([{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); expect([bodyToCompare, body[1]]).to.eql([ getSimpleRuleOutputWithoutRuleId(), - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', + error: { + status_code: 404, + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', + }, + }, ]); }); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts index e3c4233383be4..66962df14e22d 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts @@ -120,16 +120,6 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should report that it failed to import a thousand and one (10001) simple rules', async () => { - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(new Array(10001).fill('rule-1')), 'rules.ndjson') - .expect(500); - - expect(body).to.eql({ message: "Can't import more than 10000 rules", status_code: 500 }); - }); - it('should be able to read an imported rule back out correctly', async () => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts index 099ea950d128c..01138779ee39c 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules.ts @@ -207,12 +207,12 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ id: 'fake_id', name: 'some other name' }) + .send({ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }) .expect(404); expect(body).to.eql({ status_code: 404, - message: 'id: "fake_id" not found', + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', }); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts index 0bf80204e2445..26a39a46d07f7 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/patch_rules_bulk.ts @@ -261,11 +261,17 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) .set('kbn-xsrf', 'true') - .send([{ id: 'fake_id', name: 'some other name' }]) + .send([{ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }]) .expect(200); expect(body).to.eql([ - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + error: { + status_code: 404, + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', + }, + }, ]); }); @@ -333,7 +339,7 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .send([ { id: createdBody.id, name: 'some other name' }, - { id: 'fake_id', name: 'some other name' }, + { id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }, ]) .expect(200); @@ -346,10 +352,10 @@ export default ({ getService }: FtrProviderContext) => { outputRule, { error: { - message: 'id: "fake_id" not found', + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', status_code: 404, }, - id: 'fake_id', + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts index bd83ef00ccf1a..59717724e4201 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/read_rules.ts @@ -92,14 +92,14 @@ export default ({ getService }: FtrProviderContext) => { it('should return 404 if given a fake id', async () => { const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .get(`${DETECTION_ENGINE_RULES_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) .set('kbn-xsrf', 'true') .send(getSimpleRule()) .expect(404); expect(body).to.eql({ status_code: 404, - message: 'id: "fake_id" not found', + message: 'id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" not found', }); }); 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 83fb991ecef1b..127f688dfbc28 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 @@ -212,7 +212,7 @@ export default ({ getService }: FtrProviderContext) => { it('should give a 404 if it is given a fake id', async () => { const simpleRule = getSimpleRule(); - simpleRule.id = 'fake_id'; + simpleRule.id = '5096dec6-b6b9-4d8d-8f93-6c2602079d9d'; delete simpleRule.rule_id; const { body } = await supertest @@ -223,7 +223,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ status_code: 404, - message: 'id: "fake_id" not found', + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', }); }); 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 655f2b5d2e33f..54f29939fb6b4 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 @@ -269,7 +269,7 @@ export default ({ getService }: FtrProviderContext) => { it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { const ruleUpdate = getSimpleRule('rule-1'); - ruleUpdate.id = 'fake_id'; + ruleUpdate.id = '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5'; delete ruleUpdate.rule_id; const { body } = await supertest @@ -279,7 +279,13 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body).to.eql([ - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5', + error: { + status_code: 404, + message: 'id: "1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5" not found', + }, + }, ]); }); @@ -358,7 +364,7 @@ export default ({ getService }: FtrProviderContext) => { const rule2 = getSimpleRule(); delete rule2.rule_id; - rule2.id = 'fake_id'; + rule2.id = 'b3aa019a-656c-4311-b13b-4d9852e24347'; rule2.name = 'some other name'; const { body } = await supertest @@ -376,10 +382,10 @@ export default ({ getService }: FtrProviderContext) => { outputRule, { error: { - message: 'id: "fake_id" not found', + message: 'id: "b3aa019a-656c-4311-b13b-4d9852e24347" not found', status_code: 404, }, - id: 'fake_id', + id: 'b3aa019a-656c-4311-b13b-4d9852e24347', }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts index e91b735e1629b..5626d63b8f230 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts @@ -92,12 +92,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return an error if the id does not exist when trying to delete it', async () => { const { body } = await supertest - .delete(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .delete(`${DETECTION_ENGINE_RULES_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) .set('kbn-xsrf', 'true') .expect(404); expect(body).to.eql({ - message: 'id: "fake_id" not found', + message: 'id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" not found', status_code: 404, }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts index 687551c2d8313..04f168d5b7953 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts @@ -114,17 +114,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should return an error if the id does not exist when trying to delete an id', async () => { const { body } = await supertest .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: 'fake_id' }]) + .send([{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); expect(body).to.eql([ { error: { - message: 'id: "fake_id" not found', + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', status_code: 404, }, - id: 'fake_id', + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', }, ]); }); @@ -139,14 +139,20 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .send([{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); expect([bodyToCompare, body[1]]).to.eql([ getSimpleRuleOutputWithoutRuleId(), - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', + error: { + status_code: 404, + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', + }, + }, ]); }); }); @@ -241,17 +247,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should return an error if the id does not exist when trying to delete an id', async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: 'fake_id' }]) + .send([{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); expect(body).to.eql([ { error: { - message: 'id: "fake_id" not found', + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', status_code: 404, }, - id: 'fake_id', + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', }, ]); }); @@ -266,14 +272,20 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) - .send([{ id: bodyWithCreatedRule.id }, { id: 'fake_id' }]) + .send([{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }]) .set('kbn-xsrf', 'true') .expect(200); const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]); expect([bodyToCompare, body[1]]).to.eql([ getSimpleRuleOutputWithoutRuleId(), - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612', + error: { + status_code: 404, + message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found', + }, + }, ]); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts index e3c4233383be4..66962df14e22d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts @@ -120,16 +120,6 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should report that it failed to import a thousand and one (10001) simple rules', async () => { - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .attach('file', getSimpleRuleAsNdjson(new Array(10001).fill('rule-1')), 'rules.ndjson') - .expect(500); - - expect(body).to.eql({ message: "Can't import more than 10000 rules", status_code: 500 }); - }); - it('should be able to read an imported rule back out correctly', async () => { await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_import`) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts index a199382f29213..3219ae7e0e2d4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts @@ -210,12 +210,12 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .patch(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ id: 'fake_id', name: 'some other name' }) + .send({ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }) .expect(404); expect(body).to.eql({ status_code: 404, - message: 'id: "fake_id" not found', + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts index 0bf80204e2445..26a39a46d07f7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts @@ -261,11 +261,17 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .patch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`) .set('kbn-xsrf', 'true') - .send([{ id: 'fake_id', name: 'some other name' }]) + .send([{ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }]) .expect(200); expect(body).to.eql([ - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + error: { + status_code: 404, + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', + }, + }, ]); }); @@ -333,7 +339,7 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .send([ { id: createdBody.id, name: 'some other name' }, - { id: 'fake_id', name: 'some other name' }, + { id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }, ]) .expect(200); @@ -346,10 +352,10 @@ export default ({ getService }: FtrProviderContext) => { outputRule, { error: { - message: 'id: "fake_id" not found', + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', status_code: 404, }, - id: 'fake_id', + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts index bd83ef00ccf1a..59717724e4201 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts @@ -92,14 +92,14 @@ export default ({ getService }: FtrProviderContext) => { it('should return 404 if given a fake id', async () => { const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}?id=fake_id`) + .get(`${DETECTION_ENGINE_RULES_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) .set('kbn-xsrf', 'true') .send(getSimpleRule()) .expect(404); expect(body).to.eql({ status_code: 404, - message: 'id: "fake_id" not found', + message: 'id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" not found', }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 003c8645f1b22..0b1b49e379d17 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 @@ -214,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { it('should give a 404 if it is given a fake id', async () => { const simpleRule = getSimpleRule(); - simpleRule.id = 'fake_id'; + simpleRule.id = '5096dec6-b6b9-4d8d-8f93-6c2602079d9d'; delete simpleRule.rule_id; const { body } = await supertest @@ -225,7 +225,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ status_code: 404, - message: 'id: "fake_id" not found', + message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found', }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index 655f2b5d2e33f..54f29939fb6b4 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 @@ -269,7 +269,7 @@ export default ({ getService }: FtrProviderContext) => { it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { const ruleUpdate = getSimpleRule('rule-1'); - ruleUpdate.id = 'fake_id'; + ruleUpdate.id = '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5'; delete ruleUpdate.rule_id; const { body } = await supertest @@ -279,7 +279,13 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body).to.eql([ - { id: 'fake_id', error: { status_code: 404, message: 'id: "fake_id" not found' } }, + { + id: '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5', + error: { + status_code: 404, + message: 'id: "1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5" not found', + }, + }, ]); }); @@ -358,7 +364,7 @@ export default ({ getService }: FtrProviderContext) => { const rule2 = getSimpleRule(); delete rule2.rule_id; - rule2.id = 'fake_id'; + rule2.id = 'b3aa019a-656c-4311-b13b-4d9852e24347'; rule2.name = 'some other name'; const { body } = await supertest @@ -376,10 +382,10 @@ export default ({ getService }: FtrProviderContext) => { outputRule, { error: { - message: 'id: "fake_id" not found', + message: 'id: "b3aa019a-656c-4311-b13b-4d9852e24347" not found', status_code: 404, }, - id: 'fake_id', + id: 'b3aa019a-656c-4311-b13b-4d9852e24347', }, ]); });