diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts index df0084d6ff0e3..dadb6bfa4165d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts @@ -304,8 +304,29 @@ export type TimestampOverrideFallbackDisabled = z.infer; export const RequiredField = z.object({ /** @@ -368,6 +389,39 @@ export const SavedObjectResolveAliasPurpose = z.enum([ export type SavedObjectResolveAliasPurposeEnum = typeof SavedObjectResolveAliasPurpose.enum; export const SavedObjectResolveAliasPurposeEnum = SavedObjectResolveAliasPurpose.enum; +/** + * Related integration is a potential dependency of a rule. It's assumed that if the user installs +one of the related integrations of a rule, the rule might start to work properly because it will +have source events (generated by this integration) potentially matching the rule's query. + +NOTE: Proper work is not guaranteed, because a related integration, if installed, can be +configured differently or generate data that is not necessarily relevant for this rule. + +Related integration is a combination of a Fleet package and (optionally) one of the +package's "integrations" that this package contains. It is represented by 3 properties: + +- `package`: name of the package (required, unique id) +- `version`: version of the package (required, semver-compatible) +- `integration`: name of the integration of this package (optional, id within the package) + +There are Fleet packages like `windows` that contain only one integration; in this case, +`integration` should be unspecified. There are also packages like `aws` and `azure` that contain +several integrations; in this case, `integration` should be specified. + +@example +const x: RelatedIntegration = { + package: 'windows', + version: '1.5.x', +}; + +@example +const x: RelatedIntegration = { + package: 'azure', + version: '~1.1.6', + integration: 'activitylogs', +}; + + */ export type RelatedIntegration = z.infer; export const RelatedIntegration = z.object({ package: NonEmptyString, @@ -378,6 +432,22 @@ export const RelatedIntegration = z.object({ export type RelatedIntegrationArray = z.infer; export const RelatedIntegrationArray = z.array(RelatedIntegration); +/** + * Schema for fields relating to investigation fields. These are user defined fields we use to highlight +in various features in the UI such as alert details flyout and exceptions auto-population from alert. +Added in PR #163235 +Right now we only have a single field but anticipate adding more related fields to store various +configuration states such as `override` - where a user might say if they want only these fields to +display, or if they want these fields + the fields we select. When expanding this field, it may look +something like: +```typescript +const investigationFields = z.object({ + field_names: NonEmptyArray(NonEmptyString), + override: z.boolean().optional(), +}); +``` + + */ export type InvestigationFields = z.infer; export const InvestigationFields = z.object({ field_names: z.array(NonEmptyString).min(1), diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml index cd5e238723f6a..b2d72a561e46c 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml @@ -315,7 +315,28 @@ components: RequiredField: type: object - description: Describes an Elasticsearch field that is needed for the rule to function + description: | + Describes an Elasticsearch field that is needed for the rule to function. + + Almost all types of Security rules check source event documents for a match to some kind of + query or filter. If a document has certain field with certain values, then it's a match and + the rule will generate an alert. + + Required field is an event field that must be present in the source indices of a given rule. + + @example + const standardEcsField: RequiredField = { + name: 'event.action', + type: 'keyword', + ecs: true, + }; + + @example + const nonEcsField: RequiredField = { + name: 'winlog.event_data.AttributeLDAPDisplayName', + type: 'keyword', + ecs: false, + }; properties: name: $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' @@ -376,6 +397,37 @@ components: RelatedIntegration: type: object + description: | + Related integration is a potential dependency of a rule. It's assumed that if the user installs + one of the related integrations of a rule, the rule might start to work properly because it will + have source events (generated by this integration) potentially matching the rule's query. + + NOTE: Proper work is not guaranteed, because a related integration, if installed, can be + configured differently or generate data that is not necessarily relevant for this rule. + + Related integration is a combination of a Fleet package and (optionally) one of the + package's "integrations" that this package contains. It is represented by 3 properties: + + - `package`: name of the package (required, unique id) + - `version`: version of the package (required, semver-compatible) + - `integration`: name of the integration of this package (optional, id within the package) + + There are Fleet packages like `windows` that contain only one integration; in this case, + `integration` should be unspecified. There are also packages like `aws` and `azure` that contain + several integrations; in this case, `integration` should be specified. + + @example + const x: RelatedIntegration = { + package: 'windows', + version: '1.5.x', + }; + + @example + const x: RelatedIntegration = { + package: 'azure', + version: '~1.1.6', + integration: 'activitylogs', + }; properties: package: $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' @@ -392,10 +444,22 @@ components: items: $ref: '#/components/schemas/RelatedIntegration' - # Schema for fields relating to investigation fields, these are user defined fields we use to highlight in various features in the UI such as alert details flyout and exceptions auto-population from alert. Added in PR #163235 - # Right now we only have a single field but anticipate adding more related fields to store various configuration states such as `override` - where a user might say if they want only these fields to display, or if they want these fields + the fields we select. InvestigationFields: type: object + description: | + Schema for fields relating to investigation fields. These are user defined fields we use to highlight + in various features in the UI such as alert details flyout and exceptions auto-population from alert. + Added in PR #163235 + Right now we only have a single field but anticipate adding more related fields to store various + configuration states such as `override` - where a user might say if they want only these fields to + display, or if they want these fields + the fields we select. When expanding this field, it may look + something like: + ```typescript + const investigationFields = z.object({ + field_names: NonEmptyArray(NonEmptyString), + override: z.boolean().optional(), + }); + ``` properties: field_names: type: array diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.test.ts new file mode 100644 index 0000000000000..b455894333aee --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers'; +import { TimeDuration } from './time_duration'; // Update with the actual path to your TimeDuration file + +describe('TimeDuration schema', () => { + test('it should validate a correctly formed TimeDuration with time unit of seconds', () => { + const payload = '1s'; + const schema = TimeDuration({ allowedUnits: ['s'] }); + + const result = schema.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a correctly formed TimeDuration with time unit of minutes', () => { + const payload = '100m'; + const schema = TimeDuration({ allowedUnits: ['s', 'm'] }); + + const result = schema.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a correctly formed TimeDuration with time unit of hours', () => { + const payload = '10000000h'; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }); + + const result = schema.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a correctly formed TimeDuration with time unit of days', () => { + const payload = '7d'; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }); + + const result = schema.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should NOT validate a correctly formed TimeDuration with time unit of seconds if it is not an allowed unit', () => { + const payload = '30s'; + const schema = TimeDuration({ allowedUnits: ['m', 'h', 'd'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); + + test('it should NOT validate a negative TimeDuration', () => { + const payload = '-10s'; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); + + test('it should NOT validate a fractional number', () => { + const payload = '1.5s'; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); + + test('it should NOT validate a TimeDuration with an invalid time unit', () => { + const payload = '10000000days'; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); + + test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => { + const payload = '100ff0000w'; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); + + test('it should NOT validate an empty string', () => { + const payload = ''; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); + + test('it should NOT validate a number', () => { + const payload = 100; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Expected string, received number"` + ); + }); + + test('it should NOT validate a TimeDuration with a valid time unit but unsafe integer', () => { + const payload = `${Math.pow(2, 53)}h`; + const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] }); + + const result = schema.safeParse(payload); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. \\"30s\\", \\"1m\\", \\"2h\\", \\"7d\\""` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.ts new file mode 100644 index 0000000000000..da5cea87f31cf --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from 'zod'; + +type TimeUnits = 's' | 'm' | 'h' | 'd' | 'w' | 'y'; + +interface TimeDurationType { + allowedUnits: TimeUnits[]; +} + +const isTimeSafe = (time: number) => time >= 1 && Number.isSafeInteger(time); + +/** + * Types the TimeDuration as: + * - A string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time + * - in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h", "7d" + * + * Example usage: + * ``` + * const schedule: RuleSchedule = { + * interval: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).parse('3h'), + * }; + * ``` + */ +export const TimeDuration = ({ allowedUnits }: TimeDurationType) => { + return z.string().refine( + (input) => { + if (input.trim() === '') return false; + + try { + const inputLength = input.length; + const time = Number(input.trim().substring(0, inputLength - 1)); + const unit = input.trim().at(-1) as TimeUnits; + + return isTimeSafe(time) && allowedUnits.includes(unit); + } catch (error) { + return false; + } + }, + { + message: + 'Invalid time duration format. Must be a string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h", "7d"', + } + ); +}; + +export type TimeDurationSchema = ReturnType; +export type TimeDuration = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts index 310f96b7bf946..ba07c49a7b130 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/common_attributes.ts @@ -6,9 +6,24 @@ */ import * as t from 'io-ts'; -import { listArray } from '@kbn/securitysolution-io-ts-list-types'; -import { NonEmptyString, version, UUID, NonEmptyArray } from '@kbn/securitysolution-io-ts-types'; -import { max_signals, threat } from '@kbn/securitysolution-io-ts-alerting-types'; +import { NonEmptyString, UUID } from '@kbn/securitysolution-io-ts-types'; + +/* +IMPORTANT NOTE ON THIS FILE: + +This file contains the remaining rule schema types created manually via io-ts. They have been +migrated to Zod schemas created via code generation out of OpenAPI schemas +(found in x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts) + +The remaining types here couldn't easily be deleted/replaced because they are dependencies in +complex derived schemas in two files: + +- x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions/find_exception_references/find_exception_references_route.ts +- x-pack/plugins/security_solution/common/api/timeline/model/api.ts + +Once those two files are migrated to Zod, the /common/api/detection_engine/model/rule_schema_legacy +folder can be removed. +*/ export type RuleObjectId = t.TypeOf; export const RuleObjectId = UUID; @@ -24,156 +39,6 @@ export const RuleSignatureId = t.string; // should be non-empty string? export type RuleName = t.TypeOf; export const RuleName = NonEmptyString; -export type RuleDescription = t.TypeOf; -export const RuleDescription = NonEmptyString; - -export type RuleVersion = t.TypeOf; -export const RuleVersion = version; - -export type IsRuleImmutable = t.TypeOf; -export const IsRuleImmutable = t.boolean; - -export type IsRuleEnabled = t.TypeOf; -export const IsRuleEnabled = t.boolean; - -export type RuleTagArray = t.TypeOf; -export const RuleTagArray = t.array(t.string); // should be non-empty strings? - -/** - * Note that this is a non-exact io-ts type as we allow extra meta information - * to be added to the meta object - */ -export type RuleMetadata = t.TypeOf; -export const RuleMetadata = t.UnknownRecord; // should be a more specific type? - -export type RuleLicense = t.TypeOf; -export const RuleLicense = t.string; - -export type RuleAuthorArray = t.TypeOf; -export const RuleAuthorArray = t.array(t.string); // should be non-empty strings? - -export type RuleFalsePositiveArray = t.TypeOf; -export const RuleFalsePositiveArray = t.array(t.string); // should be non-empty strings? - -export type RuleReferenceArray = t.TypeOf; -export const RuleReferenceArray = t.array(t.string); // should be non-empty strings? - -export type InvestigationGuide = t.TypeOf; -export const InvestigationGuide = t.string; - -/** - * Any instructions for the user for setting up their environment in order to start receiving - * source events for a given rule. - * - * It's a multiline text. Markdown is supported. - */ -export type SetupGuide = t.TypeOf; -export const SetupGuide = t.string; - -export type BuildingBlockType = t.TypeOf; -export const BuildingBlockType = t.string; - -export type AlertsIndex = t.TypeOf; -export const AlertsIndex = t.string; - -export type AlertsIndexNamespace = t.TypeOf; -export const AlertsIndexNamespace = t.string; - -export type ExceptionListArray = t.TypeOf; -export const ExceptionListArray = listArray; - -export type MaxSignals = t.TypeOf; -export const MaxSignals = max_signals; - -export type ThreatArray = t.TypeOf; -export const ThreatArray = t.array(threat); - -export type IndexPatternArray = t.TypeOf; -export const IndexPatternArray = t.array(t.string); - -export type DataViewId = t.TypeOf; -export const DataViewId = t.string; - -export type RuleQuery = t.TypeOf; -export const RuleQuery = t.string; - -/** - * TODO: Right now the filters is an "unknown", when it could more than likely - * become the actual ESFilter as a type. - */ -export type RuleFilterArray = t.TypeOf; // Filters are not easily type-able yet -export const RuleFilterArray = t.array(t.unknown); // Filters are not easily type-able yet - -export type RuleNameOverride = t.TypeOf; -export const RuleNameOverride = t.string; // should be non-empty string? - -export type TimestampOverride = t.TypeOf; -export const TimestampOverride = t.string; // should be non-empty string? - -export type TimestampOverrideFallbackDisabled = t.TypeOf; -export const TimestampOverrideFallbackDisabled = t.boolean; - -/** - * Almost all types of Security rules check source event documents for a match to some kind of - * query or filter. If a document has certain field with certain values, then it's a match and - * the rule will generate an alert. - * - * Required field is an event field that must be present in the source indices of a given rule. - * - * @example - * const standardEcsField: RequiredField = { - * name: 'event.action', - * type: 'keyword', - * ecs: true, - * }; - * - * @example - * const nonEcsField: RequiredField = { - * name: 'winlog.event_data.AttributeLDAPDisplayName', - * type: 'keyword', - * ecs: false, - * }; - */ -export type RequiredField = t.TypeOf; -export const RequiredField = t.exact( - t.type({ - name: NonEmptyString, - type: NonEmptyString, - ecs: t.boolean, - }) -); - -/** - * Array of event fields that must be present in the source indices of a given rule. - * - * @example - * const x: RequiredFieldArray = [ - * { - * name: 'event.action', - * type: 'keyword', - * ecs: true, - * }, - * { - * name: 'event.code', - * type: 'keyword', - * ecs: true, - * }, - * { - * name: 'winlog.event_data.AttributeLDAPDisplayName', - * type: 'keyword', - * ecs: false, - * }, - * ]; - */ -export type RequiredFieldArray = t.TypeOf; -export const RequiredFieldArray = t.array(RequiredField); - -export type TimelineTemplateId = t.TypeOf; -export const TimelineTemplateId = t.string; // should be non-empty string? - -export type TimelineTemplateTitle = t.TypeOf; -export const TimelineTemplateTitle = t.string; // should be non-empty string? - /** * Outcome is a property of the saved object resolve api * will tell us info about the rule after 8.0 migrations @@ -193,96 +58,3 @@ export const SavedObjectResolveAliasPurpose = t.union([ t.literal('savedObjectConversion'), t.literal('savedObjectImport'), ]); - -/** - * Related integration is a potential dependency of a rule. It's assumed that if the user installs - * one of the related integrations of a rule, the rule might start to work properly because it will - * have source events (generated by this integration) potentially matching the rule's query. - * - * NOTE: Proper work is not guaranteed, because a related integration, if installed, can be - * configured differently or generate data that is not necessarily relevant for this rule. - * - * Related integration is a combination of a Fleet package and (optionally) one of the - * package's "integrations" that this package contains. It is represented by 3 properties: - * - * - `package`: name of the package (required, unique id) - * - `version`: version of the package (required, semver-compatible) - * - `integration`: name of the integration of this package (optional, id within the package) - * - * There are Fleet packages like `windows` that contain only one integration; in this case, - * `integration` should be unspecified. There are also packages like `aws` and `azure` that contain - * several integrations; in this case, `integration` should be specified. - * - * @example - * const x: RelatedIntegration = { - * package: 'windows', - * version: '1.5.x', - * }; - * - * @example - * const x: RelatedIntegration = { - * package: 'azure', - * version: '~1.1.6', - * integration: 'activitylogs', - * }; - */ -export type RelatedIntegration = t.TypeOf; -export const RelatedIntegration = t.exact( - t.intersection([ - t.type({ - package: NonEmptyString, - version: NonEmptyString, - }), - t.partial({ - integration: NonEmptyString, - }), - ]) -); - -/** - * Array of related integrations. - * - * @example - * const x: RelatedIntegrationArray = [ - * { - * package: 'windows', - * version: '1.5.x', - * }, - * { - * package: 'azure', - * version: '~1.1.6', - * integration: 'activitylogs', - * }, - * ]; - */ -export type RelatedIntegrationArray = t.TypeOf; -export const RelatedIntegrationArray = t.array(RelatedIntegration); - -/** - * Schema for fields relating to investigation fields, these are user defined fields we use to highlight - * in various features in the UI such as alert details flyout and exceptions auto-population from alert. - * Added in PR #163235 - * Right now we only have a single field but anticipate adding more related fields to store various - * configuration states such as `override` - where a user might say if they want only these fields to - * display, or if they want these fields + the fields we select. When expanding this field, it may look - * something like: - * export const investigationFields = t.intersection([ - * t.exact( - * t.type({ - * field_names: NonEmptyArray(NonEmptyString), - * }) - * ), - * t.exact( - * t.partial({ - * overide: t.boolean, - * }) - * ), - * ]); - * - */ -export type InvestigationFields = t.TypeOf; -export const InvestigationFields = t.exact( - t.type({ - field_names: NonEmptyArray(NonEmptyString), - }) -); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/eql_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/eql_attributes.ts deleted file mode 100644 index 0bc029fa0d4a5..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/eql_attributes.ts +++ /dev/null @@ -1,19 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -// Attributes specific to EQL rules - -export type EventCategoryOverride = t.TypeOf; -export const EventCategoryOverride = t.string; // should be non-empty string? - -export type TimestampField = t.TypeOf; -export const TimestampField = t.string; // should be non-empty string? - -export type TiebreakerField = t.TypeOf; -export const TiebreakerField = t.string; // should be non-empty string? diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts index 6fbe808a0eb48..a112f6ca1b29f 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/index.ts @@ -6,10 +6,3 @@ */ export * from './common_attributes'; - -export * from './eql_attributes'; -export * from './new_terms_attributes'; -export * from './query_attributes'; -export * from './threshold_attributes'; - -export * from './rule_schemas'; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/new_terms_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/new_terms_attributes.ts deleted file mode 100644 index 6d9f39011b675..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/new_terms_attributes.ts +++ /dev/null @@ -1,25 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { LimitedSizeArray, NonEmptyString } from '@kbn/securitysolution-io-ts-types'; -import { MAX_NUMBER_OF_NEW_TERMS_FIELDS } from '../../../../constants'; - -// Attributes specific to New Terms rules - -/** - * New terms rule type supports a limited number of fields. Max number of fields is 3 and defined in common constants as MAX_NUMBER_OF_NEW_TERMS_FIELDS - */ -export type NewTermsFields = t.TypeOf; -export const NewTermsFields = LimitedSizeArray({ - codec: t.string, - minSize: 1, - maxSize: MAX_NUMBER_OF_NEW_TERMS_FIELDS, -}); - -export type HistoryWindowStart = t.TypeOf; -export const HistoryWindowStart = NonEmptyString; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/query_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/query_attributes.ts deleted file mode 100644 index 8ee40e7a507d4..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/query_attributes.ts +++ /dev/null @@ -1,70 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { - LimitedSizeArray, - PositiveIntegerGreaterThanZero, - enumeration, -} from '@kbn/securitysolution-io-ts-types'; -import { AlertSuppressionMissingFieldsStrategyEnum } from '../rule_schema/common_attributes.gen'; - -export type AlertSuppressionMissingFields = t.TypeOf; -export const AlertSuppressionMissingFields = enumeration( - 'AlertSuppressionMissingFields', - AlertSuppressionMissingFieldsStrategyEnum -); - -export const AlertSuppressionGroupBy = LimitedSizeArray({ - codec: t.string, - minSize: 1, - maxSize: 3, -}); - -export const AlertSuppressionDuration = t.type({ - value: PositiveIntegerGreaterThanZero, - unit: t.keyof({ - s: null, - m: null, - h: null, - }), -}); - -/** - * Schema for fields relating to alert suppression, which enables limiting the number of alerts per entity. - * e.g. group_by: ['host.name'] would create only one alert per value of host.name. The created alert - * contains metadata about how many other candidate alerts with the same host.name value were suppressed. - */ -export type AlertSuppression = t.TypeOf; -export const AlertSuppression = t.intersection([ - t.exact( - t.type({ - group_by: AlertSuppressionGroupBy, - }) - ), - t.exact( - t.partial({ - duration: AlertSuppressionDuration, - missing_fields_strategy: AlertSuppressionMissingFields, - }) - ), -]); - -export type AlertSuppressionCamel = t.TypeOf; -export const AlertSuppressionCamel = t.intersection([ - t.exact( - t.type({ - groupBy: AlertSuppressionGroupBy, - }) - ), - t.exact( - t.partial({ - duration: AlertSuppressionDuration, - missingFieldsStrategy: AlertSuppressionMissingFields, - }) - ), -]); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/response_actions.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/response_actions.ts deleted file mode 100644 index e4164c6d2bb04..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/response_actions.ts +++ /dev/null @@ -1,74 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { arrayQueries, ecsMapping } from '@kbn/osquery-io-ts-types'; -import * as t from 'io-ts'; - -// to enable using RESPONSE_ACTION_API_COMMANDS_NAMES as a type -function keyObject(arr: T): { [K in T[number]]: null } { - return Object.fromEntries(arr.map((v) => [v, null])) as never; -} - -export type EndpointParams = t.TypeOf; -export const EndpointParams = t.type({ - // TODO: TC- change these when we go GA with automated process actions - command: t.keyof(keyObject(['isolate', 'kill-process', 'suspend-process'])), - // command: t.keyof(keyObject(ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS)), - comment: t.union([t.string, t.undefined]), -}); - -export const OsqueryParams = t.type({ - query: t.union([t.string, t.undefined]), - ecs_mapping: t.union([ecsMapping, t.undefined]), - queries: t.union([arrayQueries, t.undefined]), - pack_id: t.union([t.string, t.undefined]), - saved_query_id: t.union([t.string, t.undefined]), -}); - -export const OsqueryParamsCamelCase = t.type({ - query: t.union([t.string, t.undefined]), - ecsMapping: t.union([ecsMapping, t.undefined]), - queries: t.union([arrayQueries, t.undefined]), - packId: t.union([t.string, t.undefined]), - savedQueryId: t.union([t.string, t.undefined]), -}); - -// When we create new response action types, create a union of types -export type RuleResponseOsqueryAction = t.TypeOf; -export const RuleResponseOsqueryAction = t.strict({ - actionTypeId: t.literal('.osquery'), - params: OsqueryParamsCamelCase, -}); - -export type RuleResponseEndpointAction = t.TypeOf; -export const RuleResponseEndpointAction = t.strict({ - actionTypeId: t.literal('.endpoint'), - params: EndpointParams, -}); - -export type RuleResponseAction = t.TypeOf; -const ResponseActionRuleParam = t.union([RuleResponseOsqueryAction, RuleResponseEndpointAction]); - -export const ResponseActionRuleParamsOrUndefined = t.union([ - t.array(ResponseActionRuleParam), - t.undefined, -]); - -// When we create new response action types, create a union of types -const OsqueryResponseAction = t.strict({ - action_type_id: t.literal('.osquery'), - params: OsqueryParams, -}); - -const EndpointResponseAction = t.strict({ - action_type_id: t.literal('.endpoint'), - params: EndpointParams, -}); - -export type ResponseAction = t.TypeOf; -export const ResponseAction = t.union([OsqueryResponseAction, EndpointResponseAction]); - -export const ResponseActionArray = t.array(ResponseAction); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/rule_schemas.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/rule_schemas.ts deleted file mode 100644 index 4ec9ca19ee399..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/rule_schemas.ts +++ /dev/null @@ -1,388 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { - concurrent_searches, - items_per_search, - machine_learning_job_id, - RiskScore, - RiskScoreMapping, - RuleActionArray, - RuleActionThrottle, - RuleInterval, - RuleIntervalFrom, - RuleIntervalTo, - Severity, - SeverityMapping, - threat_filters, - threat_index, - threat_indicator_path, - threat_mapping, - threat_query, -} from '@kbn/securitysolution-io-ts-alerting-types'; -import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; -import { ResponseActionArray } from './response_actions'; - -import { anomaly_threshold, saved_id } from '../schemas'; - -import { - AlertsIndex, - AlertsIndexNamespace, - BuildingBlockType, - DataViewId, - ExceptionListArray, - IndexPatternArray, - InvestigationFields, - InvestigationGuide, - IsRuleEnabled, - MaxSignals, - RuleAuthorArray, - RuleDescription, - RuleFalsePositiveArray, - RuleFilterArray, - RuleLicense, - RuleMetadata, - RuleName, - RuleNameOverride, - RuleQuery, - RuleReferenceArray, - RuleSignatureId, - RuleTagArray, - RuleVersion, - SavedObjectResolveAliasPurpose, - SavedObjectResolveAliasTargetId, - SavedObjectResolveOutcome, - ThreatArray, - TimelineTemplateId, - TimelineTemplateTitle, - TimestampOverride, - TimestampOverrideFallbackDisabled, -} from './common_attributes'; -import { EventCategoryOverride, TiebreakerField, TimestampField } from './eql_attributes'; -import { HistoryWindowStart, NewTermsFields } from './new_terms_attributes'; -import { AlertSuppression } from './query_attributes'; -import { Threshold } from './threshold_attributes'; - -export const buildRuleSchemas = < - Required extends t.Props, - Optional extends t.Props, - Defaultable extends t.Props ->({ - required, - optional, - defaultable, -}: { - required: Required; - optional: Optional; - defaultable: Defaultable; -}) => ({ - create: t.intersection([ - t.exact(t.type(required)), - t.exact(t.partial(optional)), - t.exact(t.partial(defaultable)), - ]), - patch: t.intersection([t.partial(required), t.partial(optional), t.partial(defaultable)]), - response: t.intersection([ - t.exact(t.type(required)), - // This bit of logic is to force all fields to be accounted for in conversions from the internal - // rule schema to the response schema. Rather than use `t.partial`, which makes each field optional, - // we make each field required but possibly undefined. The result is that if a field is forgotten in - // the conversion from internal schema to response schema TS will report an error. If we just used t.partial - // instead, then optional fields can be accidentally omitted from the conversion - and any actual values - // in those fields internally will be stripped in the response. - t.exact(t.type(orUndefined(optional))), - t.exact(t.type(defaultable)), - ]), -}); - -export type OrUndefined

= { - [K in keyof P]: P[K] | t.UndefinedC; -}; - -export const orUndefined =

(props: P): OrUndefined

=> { - return Object.keys(props).reduce((acc, key) => { - acc[key] = t.union([props[key], t.undefined]); - return acc; - }, {}) as OrUndefined

; -}; - -// ------------------------------------------------------------------------------------------------- -// Base schema - -export const baseSchema = buildRuleSchemas({ - required: { - name: RuleName, - description: RuleDescription, - risk_score: RiskScore, - severity: Severity, - }, - optional: { - // Field overrides - rule_name_override: RuleNameOverride, - timestamp_override: TimestampOverride, - timestamp_override_fallback_disabled: TimestampOverrideFallbackDisabled, - // Timeline template - timeline_id: TimelineTemplateId, - timeline_title: TimelineTemplateTitle, - // Attributes related to SavedObjectsClient.resolve API - outcome: SavedObjectResolveOutcome, - alias_target_id: SavedObjectResolveAliasTargetId, - alias_purpose: SavedObjectResolveAliasPurpose, - // Misc attributes - license: RuleLicense, - note: InvestigationGuide, - building_block_type: BuildingBlockType, - output_index: AlertsIndex, - namespace: AlertsIndexNamespace, - meta: RuleMetadata, - investigation_fields: InvestigationFields, - // Throttle - throttle: RuleActionThrottle, - }, - defaultable: { - // Main attributes - version: RuleVersion, - tags: RuleTagArray, - enabled: IsRuleEnabled, - // Field overrides - risk_score_mapping: RiskScoreMapping, - severity_mapping: SeverityMapping, - // Rule schedule - interval: RuleInterval, - from: RuleIntervalFrom, - to: RuleIntervalTo, - // Rule actions - actions: RuleActionArray, - // Rule exceptions - exceptions_list: ExceptionListArray, - // Misc attributes - author: RuleAuthorArray, - false_positives: RuleFalsePositiveArray, - references: RuleReferenceArray, - // maxSignals not used in ML rules but probably should be used - max_signals: MaxSignals, - threat: ThreatArray, - }, -}); - -export type DurationMetric = t.TypeOf; -export const DurationMetric = PositiveInteger; - -export type RuleExecutionMetrics = t.TypeOf; - -/** - @property total_search_duration_ms - "total time spent performing ES searches as measured by Kibana; - includes network latency and time spent serializing/deserializing request/response", - @property total_indexing_duration_ms - "total time spent indexing documents during current rule execution cycle", - @property total_enrichment_duration_ms - total time spent enriching documents during current rule execution cycle - @property execution_gap_duration_s - "duration in seconds of execution gap" -*/ -export const RuleExecutionMetrics = t.partial({ - total_search_duration_ms: DurationMetric, - total_indexing_duration_ms: DurationMetric, - total_enrichment_duration_ms: DurationMetric, - execution_gap_duration_s: DurationMetric, -}); - -export type BaseCreateProps = t.TypeOf; -export const BaseCreateProps = baseSchema.create; - -// ------------------------------------------------------------------------------------------------- -// Shared schemas - -// "Shared" types are the same across all rule types, and built from "baseSchema" above -// with some variations for each route. These intersect with type specific schemas below -// to create the full schema for each route. - -export type SharedCreateProps = t.TypeOf; -export const SharedCreateProps = t.intersection([ - baseSchema.create, - t.exact(t.partial({ rule_id: RuleSignatureId })), -]); - -// ------------------------------------------------------------------------------------------------- -// EQL rule schema - -export type KqlQueryLanguage = t.TypeOf; -export const KqlQueryLanguage = t.keyof({ kuery: null, lucene: null }); - -export type EqlQueryLanguage = t.TypeOf; -export const EqlQueryLanguage = t.literal('eql'); - -const eqlSchema = buildRuleSchemas({ - required: { - type: t.literal('eql'), - language: EqlQueryLanguage, - query: RuleQuery, - }, - optional: { - index: IndexPatternArray, - data_view_id: DataViewId, - filters: RuleFilterArray, - timestamp_field: TimestampField, - event_category_override: EventCategoryOverride, - tiebreaker_field: TiebreakerField, - }, - defaultable: {}, -}); - -// ------------------------------------------------------------------------------------------------- -// ES|QL rule schema - -export type EsqlQueryLanguage = t.TypeOf; -export const EsqlQueryLanguage = t.literal('esql'); - -const esqlSchema = buildRuleSchemas({ - required: { - type: t.literal('esql'), - language: EsqlQueryLanguage, - query: RuleQuery, - }, - optional: {}, - defaultable: {}, -}); - -// ------------------------------------------------------------------------------------------------- -// Indicator Match rule schema - -const threatMatchSchema = buildRuleSchemas({ - required: { - type: t.literal('threat_match'), - query: RuleQuery, - threat_query, - threat_mapping, - threat_index, - }, - optional: { - index: IndexPatternArray, - data_view_id: DataViewId, - filters: RuleFilterArray, - saved_id, - threat_filters, - threat_indicator_path, - threat_language: KqlQueryLanguage, - concurrent_searches, - items_per_search, - }, - defaultable: { - language: KqlQueryLanguage, - }, -}); - -// ------------------------------------------------------------------------------------------------- -// Custom Query rule schema - -const querySchema = buildRuleSchemas({ - required: { - type: t.literal('query'), - }, - optional: { - index: IndexPatternArray, - data_view_id: DataViewId, - filters: RuleFilterArray, - saved_id, - response_actions: ResponseActionArray, - alert_suppression: AlertSuppression, - }, - defaultable: { - query: RuleQuery, - language: KqlQueryLanguage, - }, -}); - -// ------------------------------------------------------------------------------------------------- -// Saved Query rule schema - -const savedQuerySchema = buildRuleSchemas({ - required: { - type: t.literal('saved_query'), - saved_id, - }, - optional: { - // Having language, query, and filters possibly defined adds more code confusion and probably user confusion - // if the saved object gets deleted for some reason - index: IndexPatternArray, - data_view_id: DataViewId, - query: RuleQuery, - filters: RuleFilterArray, - response_actions: ResponseActionArray, - alert_suppression: AlertSuppression, - }, - defaultable: { - language: KqlQueryLanguage, - }, -}); - -// ------------------------------------------------------------------------------------------------- -// Threshold rule schema - -const thresholdSchema = buildRuleSchemas({ - required: { - type: t.literal('threshold'), - query: RuleQuery, - threshold: Threshold, - }, - optional: { - index: IndexPatternArray, - data_view_id: DataViewId, - filters: RuleFilterArray, - saved_id, - }, - defaultable: { - language: KqlQueryLanguage, - }, -}); - -// ------------------------------------------------------------------------------------------------- -// Machine Learning rule schema - -const machineLearningSchema = buildRuleSchemas({ - required: { - type: t.literal('machine_learning'), - anomaly_threshold, - machine_learning_job_id, - }, - optional: {}, - defaultable: {}, -}); - -// ------------------------------------------------------------------------------------------------- -// New Terms rule schema - -const newTermsSchema = buildRuleSchemas({ - required: { - type: t.literal('new_terms'), - query: RuleQuery, - new_terms_fields: NewTermsFields, - history_window_start: HistoryWindowStart, - }, - optional: { - index: IndexPatternArray, - data_view_id: DataViewId, - filters: RuleFilterArray, - }, - defaultable: { - language: KqlQueryLanguage, - }, -}); - -// ------------------------------------------------------------------------------------------------- -// Combined type specific schemas - -export type TypeSpecificCreateProps = t.TypeOf; -export const TypeSpecificCreateProps = t.union([ - eqlSchema.create, - esqlSchema.create, - threatMatchSchema.create, - querySchema.create, - savedQuerySchema.create, - thresholdSchema.create, - machineLearningSchema.create, - newTermsSchema.create, -]); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/threshold_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/threshold_attributes.ts deleted file mode 100644 index eb5639c97ab3e..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy/threshold_attributes.ts +++ /dev/null @@ -1,62 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { PositiveInteger, PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; - -// Attributes specific to Threshold rules - -const thresholdField = t.exact( - t.type({ - field: t.union([t.string, t.array(t.string)]), // Covers pre- and post-7.12 - value: PositiveIntegerGreaterThanZero, - }) -); - -const thresholdFieldNormalized = t.exact( - t.type({ - field: t.array(t.string), - value: PositiveIntegerGreaterThanZero, - }) -); - -const thresholdCardinalityField = t.exact( - t.type({ - field: t.string, - value: PositiveInteger, - }) -); - -export type Threshold = t.TypeOf; -export const Threshold = t.intersection([ - thresholdField, - t.exact( - t.partial({ - cardinality: t.array(thresholdCardinalityField), - }) - ), -]); - -export type ThresholdNormalized = t.TypeOf; -export const ThresholdNormalized = t.intersection([ - thresholdFieldNormalized, - t.exact( - t.partial({ - cardinality: t.array(thresholdCardinalityField), - }) - ), -]); - -export type ThresholdWithCardinality = t.TypeOf; -export const ThresholdWithCardinality = t.intersection([ - thresholdFieldNormalized, - t.exact( - t.type({ - cardinality: t.array(thresholdCardinalityField), - }) - ), -]); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/schemas.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/schemas.ts index 68a6dbb461673..12973846c1f23 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/schemas.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/schemas.ts @@ -7,89 +7,76 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import * as t from 'io-ts'; -import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; - -export const file_name = t.string; -export type FileName = t.TypeOf; - -export const exclude_export_details = t.boolean; -export type ExcludeExportDetails = t.TypeOf; - -export const saved_id = t.string; - -export const savedIdOrUndefined = t.union([saved_id, t.undefined]); -export type SavedIdOrUndefined = t.TypeOf; - -export const anomaly_threshold = PositiveInteger; - -export const status = t.keyof({ - open: null, - closed: null, - acknowledged: null, - 'in-progress': null, -}); -export type Status = t.TypeOf; - -export const conflicts = t.keyof({ abort: null, proceed: null }); - -export const signal_ids = t.array(t.string); -export type SignalIds = t.TypeOf; - -// 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 alert_tag_ids = t.array(t.string); -export type AlertTagIds = t.TypeOf; - -export const indexRecord = t.record( - t.string, - t.type({ - all: t.boolean, - maintenance: t.boolean, - read: t.boolean, - create_index: t.boolean, - index: t.boolean, - monitor: t.boolean, - delete: t.boolean, - manage: t.boolean, - delete_index: t.boolean, - create_doc: t.boolean, - view_index_metadata: t.boolean, - create: t.boolean, - write: t.boolean, +import { z } from 'zod'; + +export type FileName = z.infer; +export const file_name = z.string(); + +export type ExcludeExportDetails = z.infer; +export const exclude_export_details = z.boolean(); + +export const saved_id = z.string(); + +export type SavedIdOrUndefined = z.infer; +export const savedIdOrUndefined = saved_id.optional(); + +export const status = z.enum(['open', 'closed', 'acknowledged', 'in-progress']); +export type Status = z.infer; + +export const signal_ids = z.array(z.string()); +export type SignalIds = z.infer; + +export const alert_tag_ids = z.array(z.string()); +export type AlertTagIds = z.infer; + +export const indexRecord = z.record( + z.string(), + z.object({ + all: z.boolean(), + maintenance: z.boolean(), + read: z.boolean(), + create_index: z.boolean(), + index: z.boolean(), + monitor: z.boolean(), + delete: z.boolean(), + manage: z.boolean(), + delete_index: z.boolean(), + create_doc: z.boolean(), + view_index_metadata: z.boolean(), + create: z.boolean(), + write: z.boolean(), }) ); -export const privilege = t.type({ - username: t.string, - has_all_requested: t.boolean, - cluster: t.type({ - monitor_ml: t.boolean, - manage_index_templates: t.boolean, - monitor_transform: t.boolean, - manage_security: t.boolean, - manage_own_api_key: t.boolean, - all: t.boolean, - monitor: t.boolean, - manage: t.boolean, - manage_transform: t.boolean, - manage_ml: t.boolean, - manage_pipeline: t.boolean, +export const privilege = z.object({ + username: z.string(), + has_all_requested: z.boolean(), + cluster: z.object({ + monitor_ml: z.boolean(), + manage_index_templates: z.boolean(), + monitor_transform: z.boolean(), + manage_security: z.boolean(), + manage_own_api_key: z.boolean(), + all: z.boolean(), + monitor: z.boolean(), + manage: z.boolean(), + manage_transform: z.boolean(), + manage_ml: z.boolean(), + manage_pipeline: z.boolean(), }), index: indexRecord, - is_authenticated: t.boolean, - has_encryption_key: t.boolean, + is_authenticated: z.boolean(), + has_encryption_key: z.boolean(), }); -export type Privilege = t.TypeOf; +export type Privilege = z.infer; -export const alert_tags = t.type({ - tags_to_add: t.array(t.string), - tags_to_remove: t.array(t.string), +export const alert_tags = z.object({ + tags_to_add: z.array(z.string()), + tags_to_remove: z.array(z.string()), }); -export type AlertTags = t.TypeOf; +export type AlertTags = z.infer; -export const user_search_term = t.string; -export type UserSearchTerm = t.TypeOf; +export const user_search_term = z.string(); +export type UserSearchTerm = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts index a2b514676767b..635dfdf45c1c4 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts @@ -14,7 +14,6 @@ export * from './review_rule_installation/review_rule_installation_route'; export * from './review_rule_upgrade/review_rule_upgrade_route'; export * from './urls'; export * from './model/aggregated_prebuilt_rules_error'; -export * from './model/diff/diffable_rule/build_schema'; export * from './model/diff/diffable_rule/diffable_field_types'; export * from './model/diff/diffable_rule/diffable_rule'; export * from './model/diff/rule_diff/fields_diff'; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/build_schema.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/build_schema.ts deleted file mode 100644 index b1d7752fb9f89..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/build_schema.ts +++ /dev/null @@ -1,25 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -// TODO https://github.com/elastic/security-team/issues/7491 -// eslint-disable-next-line no-restricted-imports -import { orUndefined } from '../../../../model/rule_schema_legacy'; - -interface RuleFields { - required: TRequired; - optional: TOptional; -} - -export const buildSchema = ( - fields: RuleFields -) => { - return t.intersection([ - t.exact(t.type(fields.required)), - t.exact(t.type(orUndefined(fields.optional))), - ]); -}; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_field_types.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_field_types.ts index 299b4a7d7b394..aa64c1d12185a 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_field_types.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_field_types.ts @@ -4,25 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { z } from 'zod'; -import * as t from 'io-ts'; -import { TimeDuration } from '@kbn/securitysolution-io-ts-types'; -// TODO https://github.com/elastic/security-team/issues/7491 -// eslint-disable-next-line no-restricted-imports import { BuildingBlockType, DataViewId, IndexPatternArray, KqlQueryLanguage, RuleFilterArray, - RuleNameOverride as RuleNameOverrideFieldName, + RuleNameOverride, RuleQuery, + SavedQueryId, TimelineTemplateId, TimelineTemplateTitle, - TimestampOverride as TimestampOverrideFieldName, + TimestampOverride, TimestampOverrideFallbackDisabled, -} from '../../../../model/rule_schema_legacy'; -import { saved_id } from '../../../../model/schemas'; +} from '../../../../model/rule_schema'; +import { TimeDuration } from '../../../../model/rule_schema/time_duration'; // ------------------------------------------------------------------------------------------------- // Rule data source @@ -32,24 +30,23 @@ export enum DataSourceType { 'data_view' = 'data_view', } -export type DataSourceIndexPatterns = t.TypeOf; -export const DataSourceIndexPatterns = t.exact( - t.type({ - type: t.literal(DataSourceType.index_patterns), - index_patterns: IndexPatternArray, - }) -); - -export type DataSourceDataView = t.TypeOf; -export const DataSourceDataView = t.exact( - t.type({ - type: t.literal(DataSourceType.data_view), - data_view_id: DataViewId, - }) -); - -export type RuleDataSource = t.TypeOf; -export const RuleDataSource = t.union([DataSourceIndexPatterns, DataSourceDataView]); +export type DataSourceIndexPatterns = z.infer; +export const DataSourceIndexPatterns = z.object({ + type: z.literal(DataSourceType.index_patterns), + index_patterns: IndexPatternArray, +}); + +export type DataSourceDataView = z.infer; +export const DataSourceDataView = z.object({ + type: z.literal(DataSourceType.data_view), + data_view_id: DataViewId, +}); + +export type RuleDataSource = z.infer; +export const RuleDataSource = z.discriminatedUnion('type', [ + DataSourceIndexPatterns, + DataSourceDataView, +]); // ------------------------------------------------------------------------------------------------- // Rule data query @@ -59,93 +56,75 @@ export enum KqlQueryType { 'saved_query' = 'saved_query', } -export type InlineKqlQuery = t.TypeOf; -export const InlineKqlQuery = t.exact( - t.type({ - type: t.literal(KqlQueryType.inline_query), - query: RuleQuery, - language: KqlQueryLanguage, - filters: RuleFilterArray, - }) -); - -export type SavedKqlQuery = t.TypeOf; -export const SavedKqlQuery = t.exact( - t.type({ - type: t.literal(KqlQueryType.saved_query), - saved_query_id: saved_id, - }) -); - -export type RuleKqlQuery = t.TypeOf; -export const RuleKqlQuery = t.union([InlineKqlQuery, SavedKqlQuery]); - -export type RuleEqlQuery = t.TypeOf; -export const RuleEqlQuery = t.exact( - t.type({ - query: RuleQuery, - language: t.literal('eql'), - filters: RuleFilterArray, - }) -); - -export type RuleEsqlQuery = t.TypeOf; -export const RuleEsqlQuery = t.exact( - t.type({ - query: RuleQuery, - language: t.literal('esql'), - }) -); +export type InlineKqlQuery = z.infer; +export const InlineKqlQuery = z.object({ + type: z.literal(KqlQueryType.inline_query), + query: RuleQuery, + language: KqlQueryLanguage, + filters: RuleFilterArray, +}); + +export type SavedKqlQuery = z.infer; +export const SavedKqlQuery = z.object({ + type: z.literal(KqlQueryType.saved_query), + saved_query_id: SavedQueryId, +}); + +export type RuleKqlQuery = z.infer; +export const RuleKqlQuery = z.discriminatedUnion('type', [InlineKqlQuery, SavedKqlQuery]); + +export type RuleEqlQuery = z.infer; +export const RuleEqlQuery = z.object({ + query: RuleQuery, + language: z.literal('eql'), + filters: RuleFilterArray, +}); + +export type RuleEsqlQuery = z.infer; +export const RuleEsqlQuery = z.object({ + query: RuleQuery, + language: z.literal('esql'), +}); // ------------------------------------------------------------------------------------------------- // Rule schedule -export type RuleSchedule = t.TypeOf; -export const RuleSchedule = t.exact( - t.type({ - interval: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }), - lookback: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }), - }) -); +export type RuleSchedule = z.infer; +export const RuleSchedule = z.object({ + interval: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }), + lookback: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }), +}); // ------------------------------------------------------------------------------------------------- // Rule name override -export type RuleNameOverrideObject = t.TypeOf; -export const RuleNameOverrideObject = t.exact( - t.type({ - field_name: RuleNameOverrideFieldName, - }) -); +export type RuleNameOverrideObject = z.infer; +export const RuleNameOverrideObject = z.object({ + field_name: RuleNameOverride, +}); // ------------------------------------------------------------------------------------------------- // Timestamp override -export type TimestampOverrideObject = t.TypeOf; -export const TimestampOverrideObject = t.exact( - t.type({ - field_name: TimestampOverrideFieldName, - fallback_disabled: TimestampOverrideFallbackDisabled, - }) -); +export type TimestampOverrideObject = z.infer; +export const TimestampOverrideObject = z.object({ + field_name: TimestampOverride, + fallback_disabled: TimestampOverrideFallbackDisabled, +}); // ------------------------------------------------------------------------------------------------- // Reference to a timeline template -export type TimelineTemplateReference = t.TypeOf; -export const TimelineTemplateReference = t.exact( - t.type({ - timeline_id: TimelineTemplateId, - timeline_title: TimelineTemplateTitle, - }) -); +export type TimelineTemplateReference = z.infer; +export const TimelineTemplateReference = z.object({ + timeline_id: TimelineTemplateId, + timeline_title: TimelineTemplateTitle, +}); // ------------------------------------------------------------------------------------------------- // Building block -export type BuildingBlockObject = t.TypeOf; -export const BuildingBlockObject = t.exact( - t.type({ - type: BuildingBlockType, - }) -); +export type BuildingBlockObject = z.infer; +export const BuildingBlockObject = z.object({ + type: BuildingBlockType, +}); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts index 9bb6fc10031d2..3d58f1e0b7da5 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts @@ -5,212 +5,164 @@ * 2.0. */ -import * as t from 'io-ts'; +import { z } from 'zod'; import { - concurrent_searches, - items_per_search, - machine_learning_job_id, - RiskScore, - RiskScoreMapping, - RuleActionArray, - RuleActionThrottle, - Severity, - SeverityMapping, - threat_index, - threat_indicator_path, - threat_mapping, -} from '@kbn/securitysolution-io-ts-alerting-types'; - -// TODO https://github.com/elastic/security-team/issues/7491 -// eslint-disable-next-line no-restricted-imports -import { - AlertSuppression, + AnomalyThreshold, + ConcurrentSearches, EventCategoryOverride, - ExceptionListArray, HistoryWindowStart, InvestigationGuide, + ItemsPerSearch, + MachineLearningJobId, MaxSignals, NewTermsFields, RelatedIntegrationArray, RequiredFieldArray, + RiskScore, + RiskScoreMapping, RuleAuthorArray, RuleDescription, + RuleExceptionList, RuleFalsePositiveArray, RuleLicense, - RuleMetadata, RuleName, RuleReferenceArray, RuleSignatureId, RuleTagArray, RuleVersion, SetupGuide, + Severity, + SeverityMapping, ThreatArray, + ThreatIndex, + ThreatIndicatorPath, + ThreatMapping, Threshold, TiebreakerField, TimestampField, -} from '../../../../model/rule_schema_legacy'; +} from '../../../../model/rule_schema'; import { BuildingBlockObject, + InlineKqlQuery, + RuleDataSource, RuleEqlQuery, RuleEsqlQuery, - InlineKqlQuery, RuleKqlQuery, - RuleDataSource, RuleNameOverrideObject, RuleSchedule, TimelineTemplateReference, TimestampOverrideObject, } from './diffable_field_types'; -import { buildSchema } from './build_schema'; -import { anomaly_threshold } from '../../../../model/schemas'; - -export type DiffableCommonFields = t.TypeOf; -export const DiffableCommonFields = buildSchema({ - required: { - // Technical fields - // NOTE: We might consider removing them from the schema and returning from the API - // not via the fields diff, but via dedicated properties in the response body. - rule_id: RuleSignatureId, - version: RuleVersion, - meta: RuleMetadata, - - // Main domain fields - name: RuleName, - tags: RuleTagArray, - description: RuleDescription, - severity: Severity, - severity_mapping: SeverityMapping, - risk_score: RiskScore, - risk_score_mapping: RiskScoreMapping, - - // About -> Advanced settings - references: RuleReferenceArray, - false_positives: RuleFalsePositiveArray, - threat: ThreatArray, - note: InvestigationGuide, - setup: SetupGuide, - related_integrations: RelatedIntegrationArray, - required_fields: RequiredFieldArray, - author: RuleAuthorArray, - license: RuleLicense, - - // Other domain fields - rule_schedule: RuleSchedule, // NOTE: new field - actions: RuleActionArray, - throttle: RuleActionThrottle, - exceptions_list: ExceptionListArray, - max_signals: MaxSignals, - }, - optional: { - rule_name_override: RuleNameOverrideObject, // NOTE: new field - timestamp_override: TimestampOverrideObject, // NOTE: new field - timeline_template: TimelineTemplateReference, // NOTE: new field - building_block: BuildingBlockObject, // NOTE: new field - }, +export type DiffableCommonFields = z.infer; +export const DiffableCommonFields = z.object({ + // Technical fields + // NOTE: We might consider removing them from the schema and returning from the API + // not via the fields diff, but via dedicated properties in the response body. + rule_id: RuleSignatureId, + version: RuleVersion, + + // Main domain fields + name: RuleName, + tags: RuleTagArray, + description: RuleDescription, + severity: Severity, + severity_mapping: SeverityMapping, + risk_score: RiskScore, + risk_score_mapping: RiskScoreMapping, + + // About -> Advanced settings + references: RuleReferenceArray, + false_positives: RuleFalsePositiveArray, + threat: ThreatArray, + note: InvestigationGuide, + setup: SetupGuide, + related_integrations: RelatedIntegrationArray, + required_fields: RequiredFieldArray, + author: RuleAuthorArray, + license: RuleLicense, + + // Other domain fields + rule_schedule: RuleSchedule, // NOTE: new field + exceptions_list: z.array(RuleExceptionList), + max_signals: MaxSignals, + + // Optional fields + rule_name_override: RuleNameOverrideObject.optional(), // NOTE: new field + timestamp_override: TimestampOverrideObject.optional(), // NOTE: new field + timeline_template: TimelineTemplateReference.optional(), // NOTE: new field + building_block: BuildingBlockObject.optional(), // NOTE: new field }); -export type DiffableCustomQueryFields = t.TypeOf; -export const DiffableCustomQueryFields = buildSchema({ - required: { - type: t.literal('query'), - kql_query: RuleKqlQuery, // NOTE: new field - }, - optional: { - data_source: RuleDataSource, // NOTE: new field - alert_suppression: AlertSuppression, - }, +export type DiffableCustomQueryFields = z.infer; +export const DiffableCustomQueryFields = z.object({ + type: z.literal('query'), + kql_query: RuleKqlQuery, // NOTE: new field + data_source: RuleDataSource.optional(), // NOTE: new field }); -export type DiffableSavedQueryFields = t.TypeOf; -export const DiffableSavedQueryFields = buildSchema({ - required: { - type: t.literal('saved_query'), - kql_query: RuleKqlQuery, // NOTE: new field - }, - optional: { - data_source: RuleDataSource, // NOTE: new field - alert_suppression: AlertSuppression, - }, +export type DiffableSavedQueryFields = z.infer; +export const DiffableSavedQueryFields = z.object({ + type: z.literal('saved_query'), + kql_query: RuleKqlQuery, // NOTE: new field + data_source: RuleDataSource.optional(), // NOTE: new field }); -export type DiffableEqlFields = t.TypeOf; -export const DiffableEqlFields = buildSchema({ - required: { - type: t.literal('eql'), - eql_query: RuleEqlQuery, // NOTE: new field - }, - optional: { - data_source: RuleDataSource, // NOTE: new field - event_category_override: EventCategoryOverride, - timestamp_field: TimestampField, - tiebreaker_field: TiebreakerField, - }, +export type DiffableEqlFields = z.infer; +export const DiffableEqlFields = z.object({ + type: z.literal('eql'), + eql_query: RuleEqlQuery, // NOTE: new field + data_source: RuleDataSource.optional(), // NOTE: new field + event_category_override: EventCategoryOverride.optional(), + timestamp_field: TimestampField.optional(), + tiebreaker_field: TiebreakerField.optional(), }); -export type DiffableEsqlFields = t.TypeOf; -export const DiffableEsqlFields = buildSchema({ - required: { - type: t.literal('esql'), - esql_query: RuleEsqlQuery, // NOTE: new field - }, - // this is a new type of rule, no prebuilt rules created yet. - // new properties might be added here during further rule type development - optional: {}, +// this is a new type of rule, no prebuilt rules created yet. +// new properties might be added here during further rule type development +export type DiffableEsqlFields = z.infer; +export const DiffableEsqlFields = z.object({ + type: z.literal('esql'), + esql_query: RuleEsqlQuery, // NOTE: new field }); -export type DiffableThreatMatchFields = t.TypeOf; -export const DiffableThreatMatchFields = buildSchema({ - required: { - type: t.literal('threat_match'), - kql_query: RuleKqlQuery, // NOTE: new field - threat_query: InlineKqlQuery, // NOTE: new field - threat_index, - threat_mapping, - }, - optional: { - data_source: RuleDataSource, // NOTE: new field - threat_indicator_path, - concurrent_searches, // Should combine concurrent_searches and items_per_search? - items_per_search, - }, +export type DiffableThreatMatchFields = z.infer; +export const DiffableThreatMatchFields = z.object({ + type: z.literal('threat_match'), + kql_query: RuleKqlQuery, // NOTE: new field + threat_query: InlineKqlQuery, // NOTE: new field + threat_index: ThreatIndex, + threat_mapping: ThreatMapping, + data_source: RuleDataSource.optional(), // NOTE: new field + threat_indicator_path: ThreatIndicatorPath.optional(), + concurrent_searches: ConcurrentSearches.optional(), + items_per_search: ItemsPerSearch.optional(), }); -export type DiffableThresholdFields = t.TypeOf; -export const DiffableThresholdFields = buildSchema({ - required: { - type: t.literal('threshold'), - kql_query: RuleKqlQuery, // NOTE: new field - threshold: Threshold, - }, - optional: { - data_source: RuleDataSource, // NOTE: new field - }, +export type DiffableThresholdFields = z.infer; +export const DiffableThresholdFields = z.object({ + type: z.literal('threshold'), + kql_query: RuleKqlQuery, // NOTE: new field + threshold: Threshold, + data_source: RuleDataSource.optional(), // NOTE: new field }); -export type DiffableMachineLearningFields = t.TypeOf; -export const DiffableMachineLearningFields = buildSchema({ - required: { - type: t.literal('machine_learning'), - machine_learning_job_id, - anomaly_threshold, - }, - optional: {}, +export type DiffableMachineLearningFields = z.infer; +export const DiffableMachineLearningFields = z.object({ + type: z.literal('machine_learning'), + machine_learning_job_id: MachineLearningJobId, + anomaly_threshold: AnomalyThreshold, }); -export type DiffableNewTermsFields = t.TypeOf; -export const DiffableNewTermsFields = buildSchema({ - required: { - type: t.literal('new_terms'), - kql_query: InlineKqlQuery, // NOTE: new field - new_terms_fields: NewTermsFields, - history_window_start: HistoryWindowStart, - }, - optional: { - data_source: RuleDataSource, // NOTE: new field - }, +export type DiffableNewTermsFields = z.infer; +export const DiffableNewTermsFields = z.object({ + type: z.literal('new_terms'), + kql_query: InlineKqlQuery, // NOTE: new field + new_terms_fields: NewTermsFields, + history_window_start: HistoryWindowStart, + data_source: RuleDataSource.optional(), // NOTE: new field }); /** @@ -240,10 +192,10 @@ export const DiffableNewTermsFields = buildSchema({ * top-level fields. */ -export type DiffableRule = t.TypeOf; -export const DiffableRule = t.intersection([ +export type DiffableRule = z.infer; +const DiffableRule = z.intersection( DiffableCommonFields, - t.union([ + z.discriminatedUnion('type', [ DiffableCustomQueryFields, DiffableSavedQueryFields, DiffableEqlFields, @@ -252,8 +204,8 @@ export const DiffableRule = t.intersection([ DiffableThresholdFields, DiffableMachineLearningFields, DiffableNewTermsFields, - ]), -]); + ]) +); /** * This is a merge of all fields from all rule types into a single TS type. diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/fields_diff.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/fields_diff.ts index a1b8a0ae8d104..9637faebe9c9c 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/fields_diff.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/rule_diff/fields_diff.ts @@ -7,10 +7,10 @@ import type { ThreeWayDiff, ThreeWayDiffAlgorithm } from '../three_way_diff/three_way_diff'; -export type FieldsDiff = { +export type FieldsDiff = Required<{ [Field in keyof TObject]: ThreeWayDiff; -}; +}>; -export type FieldsDiffAlgorithmsFor = { +export type FieldsDiffAlgorithmsFor = Required<{ [Field in keyof TObject]: ThreeWayDiffAlgorithm; -}; +}>; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/index.ts index 7e550633d8efc..8abee3cbe83a6 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/index.ts @@ -6,7 +6,6 @@ */ export * from './aggregated_prebuilt_rules_error'; -export * from './diff/diffable_rule/build_schema'; export * from './diff/diffable_rule/diffable_field_types'; export * from './diff/diffable_rule/diffable_rule'; export * from './diff/rule_diff/fields_diff'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts index 45cb8f0633e65..0672e599160ad 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts @@ -51,7 +51,6 @@ export const DEFINITION_UPGRADE_FIELD_ORDER: Array = [ 'threat_indicator_path', 'concurrent_searches', 'items_per_search', - 'alert_suppression', 'new_terms_fields', 'history_window_start', 'max_signals', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_field_diffs_for_grouped_fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_field_diffs_for_grouped_fields.ts index 2c5c20da928dd..21717998483ea 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_field_diffs_for_grouped_fields.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/per_field_diff/get_field_diffs_for_grouped_fields.ts @@ -8,6 +8,9 @@ import stringify from 'json-stable-stringify'; import type { AllFieldsDiff, + RuleFieldsDiffWithDataSource, + RuleFieldsDiffWithEqlQuery, + RuleFieldsDiffWithEsqlQuery, RuleFieldsDiffWithKqlQuery, } from '../../../../../../common/api/detection_engine'; import type { FieldDiff } from '../../../model/rule_details/rule_field_diff'; @@ -24,7 +27,7 @@ export const sortAndStringifyJson = (fieldValue: unknown): string => { }; export const getFieldDiffsForDataSource = ( - dataSourceThreeWayDiff: AllFieldsDiff['data_source'] + dataSourceThreeWayDiff: RuleFieldsDiffWithDataSource['data_source'] ): FieldDiff[] => { const currentType = sortAndStringifyJson(dataSourceThreeWayDiff.current_version?.type); const targetType = sortAndStringifyJson(dataSourceThreeWayDiff.target_version?.type); @@ -171,7 +174,9 @@ export const getFieldDiffsForKqlQuery = ( ]; }; -export const getFieldDiffsForEqlQuery = (eqlQuery: AllFieldsDiff['eql_query']): FieldDiff[] => { +export const getFieldDiffsForEqlQuery = ( + eqlQuery: RuleFieldsDiffWithEqlQuery['eql_query'] +): FieldDiff[] => { const currentQuery = sortAndStringifyJson(eqlQuery.current_version?.query); const targetQuery = sortAndStringifyJson(eqlQuery.target_version?.query); @@ -199,7 +204,9 @@ export const getFieldDiffsForEqlQuery = (eqlQuery: AllFieldsDiff['eql_query']): ]; }; -export const getFieldDiffsForEsqlQuery = (esqlQuery: AllFieldsDiff['esql_query']): FieldDiff[] => { +export const getFieldDiffsForEsqlQuery = ( + esqlQuery: RuleFieldsDiffWithEsqlQuery['esql_query'] +): FieldDiff[] => { const currentQuery = sortAndStringifyJson(esqlQuery.current_version?.query); const targetQuery = sortAndStringifyJson(esqlQuery.target_version?.query); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx index b22fc1b73bbe6..dd6d0417d3bb6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_diff_tab.tsx @@ -28,7 +28,7 @@ import * as i18n from './json_diff/translations'; import { getHumanizedDuration } from '../../../../detections/pages/detection_engine/rules/helpers'; /* Inclding these properties in diff display might be confusing to users. */ -const HIDDEN_PROPERTIES = [ +const HIDDEN_PROPERTIES: Array = [ /* By default, prebuilt rules don't have any actions or exception lists. So if a user has defined actions or exception lists for a rule, it'll show up as diff. This looks confusing as the user might think that their actions and exceptions lists will get removed after the upgrade, which is not the case - they will be preserved. */ @@ -44,13 +44,13 @@ const HIDDEN_PROPERTIES = [ 'revision', /* - "updated_at" value is regenerated on every '/upgrade/_review' endpoint run + "updated_at" value is regenerated on every '/upgrade/_review' endpoint run and will therefore always show a diff. It adds no value to display it to the user. */ 'updated_at', /* - These values make sense only for installed prebuilt rules. + These values make sense only for installed prebuilt rules. They are not present in the prebuilt rule package. So, showing them in the diff doesn't add value. */ @@ -76,11 +76,11 @@ const normalizeRule = (originalRule: RuleResponse): RuleResponse => { const rule = { ...originalRule }; /* - Convert the "from" property value to a humanized duration string, like 'now-1m' or 'now-2h'. - Conversion is needed to skip showing the diff for the "from" property when the same - duration is represented in different time units. For instance, 'now-1h' and 'now-3600s' + Convert the "from" property value to a humanized duration string, like 'now-1m' or 'now-2h'. + Conversion is needed to skip showing the diff for the "from" property when the same + duration is represented in different time units. For instance, 'now-1h' and 'now-3600s' indicate a one-hour duration. - The same helper is used in the rule editing UI to format "from" before submitting the edits. + The same helper is used in the rule editing UI to format "from" before submitting the edits. So, after the rule is saved, the "from" property unit/value might differ from what's in the package. */ rule.from = formatScheduleStepData({ @@ -91,8 +91,8 @@ const normalizeRule = (originalRule: RuleResponse): RuleResponse => { /* Default "note" to an empty string if it's not present. - Sometimes, in a new version of a rule, the "note" value equals an empty string, while - in the old version, it wasn't specified at all (undefined becomes ''). In this case, + Sometimes, in a new version of a rule, the "note" value equals an empty string, while + in the old version, it wasn't specified at all (undefined becomes ''). In this case, it doesn't make sense to show diff, so we default falsy values to ''. */ rule.note = rule.note ?? ''; @@ -104,9 +104,9 @@ const normalizeRule = (originalRule: RuleResponse): RuleResponse => { rule.threat = filterEmptyThreats(rule.threat); /* - The "machine_learning_job_id" property is converted from the legacy string format - to the new array format during installation and upgrade. Thus, all installed rules - use the new format. For correct comparison, we must ensure that the rule update is + The "machine_learning_job_id" property is converted from the legacy string format + to the new array format during installation and upgrade. Thus, all installed rules + use the new format. For correct comparison, we must ensure that the rule update is also in the new format before showing the diff. */ if ('machine_learning_job_id' in rule) { @@ -115,7 +115,7 @@ const normalizeRule = (originalRule: RuleResponse): RuleResponse => { /* Default the "alias" property to null for all threat filters that don't have it. - Setting a default is needed to match the behavior of the rule editing UI, + Setting a default is needed to match the behavior of the rule editing UI, which also defaults the "alias" property to null. */ if (rule.type === 'threat_match' && Array.isArray(rule.threat_filters)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index ea482858650fb..5e9bd8bd9ae64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -173,7 +173,6 @@ const calculateCommonFieldsDiff = ( const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { rule_id: simpleDiffAlgorithm, version: numberDiffAlgorithm, - meta: simpleDiffAlgorithm, name: singleLineStringDiffAlgorithm, tags: simpleDiffAlgorithm, description: simpleDiffAlgorithm, @@ -191,8 +190,6 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor author: simpleDiffAlgorithm, license: singleLineStringDiffAlgorithm, rule_schedule: simpleDiffAlgorithm, - actions: simpleDiffAlgorithm, - throttle: simpleDiffAlgorithm, exceptions_list: simpleDiffAlgorithm, max_signals: numberDiffAlgorithm, rule_name_override: simpleDiffAlgorithm, @@ -211,7 +208,6 @@ const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor { +): RequiredOptional => { return { // --------------------- REQUIRED FIELDS // Technical fields rule_id: rule.rule_id, version: rule.version, - meta: rule.meta ?? {}, // Main domain fields name: rule.name, @@ -140,8 +139,6 @@ const extractDiffableCommonFields = ( // Other domain fields rule_schedule: extractRuleSchedule(rule), - actions: (rule.actions ?? []) as RuleActionArray, - throttle: rule.throttle ?? 'no_actions', exceptions_list: rule.exceptions_list ?? [], max_signals: rule.max_signals ?? DEFAULT_MAX_SIGNALS, @@ -155,29 +152,27 @@ const extractDiffableCommonFields = ( const extractDiffableCustomQueryFields = ( rule: QueryRule | QueryRuleCreateProps -): DiffableCustomQueryFields => { +): RequiredOptional => { return { type: rule.type, kql_query: extractRuleKqlQuery(rule.query, rule.language, rule.filters, rule.saved_id), data_source: extractRuleDataSource(rule.index, rule.data_view_id), - alert_suppression: rule.alert_suppression, }; }; const extractDiffableSavedQueryFieldsFromRuleObject = ( rule: SavedQueryRule | SavedQueryRuleCreateProps -): DiffableSavedQueryFields => { +): RequiredOptional => { return { type: rule.type, kql_query: extractRuleKqlQuery(rule.query, rule.language, rule.filters, rule.saved_id), data_source: extractRuleDataSource(rule.index, rule.data_view_id), - alert_suppression: rule.alert_suppression, }; }; const extractDiffableEqlFieldsFromRuleObject = ( rule: EqlRule | EqlRuleCreateProps -): DiffableEqlFields => { +): RequiredOptional => { return { type: rule.type, eql_query: extractRuleEqlQuery(rule.query, rule.language, rule.filters), @@ -190,7 +185,7 @@ const extractDiffableEqlFieldsFromRuleObject = ( const extractDiffableEsqlFieldsFromRuleObject = ( rule: EsqlRule | EsqlRuleCreateProps -): DiffableEsqlFields => { +): RequiredOptional => { return { type: rule.type, esql_query: extractRuleEsqlQuery(rule.query, rule.language), @@ -199,7 +194,7 @@ const extractDiffableEsqlFieldsFromRuleObject = ( const extractDiffableThreatMatchFieldsFromRuleObject = ( rule: ThreatMatchRule | ThreatMatchRuleCreateProps -): DiffableThreatMatchFields => { +): RequiredOptional => { return { type: rule.type, kql_query: extractRuleKqlQuery(rule.query, rule.language, rule.filters, rule.saved_id), @@ -219,7 +214,7 @@ const extractDiffableThreatMatchFieldsFromRuleObject = ( const extractDiffableThresholdFieldsFromRuleObject = ( rule: ThresholdRule | ThresholdRuleCreateProps -): DiffableThresholdFields => { +): RequiredOptional => { return { type: rule.type, kql_query: extractRuleKqlQuery(rule.query, rule.language, rule.filters, rule.saved_id), @@ -230,7 +225,7 @@ const extractDiffableThresholdFieldsFromRuleObject = ( const extractDiffableMachineLearningFieldsFromRuleObject = ( rule: MachineLearningRule | MachineLearningRuleCreateProps -): DiffableMachineLearningFields => { +): RequiredOptional => { return { type: rule.type, machine_learning_job_id: rule.machine_learning_job_id, @@ -240,7 +235,7 @@ const extractDiffableMachineLearningFieldsFromRuleObject = ( const extractDiffableNewTermsFieldsFromRuleObject = ( rule: NewTermsRule | NewTermsRuleCreateProps -): DiffableNewTermsFields => { +): RequiredOptional => { return { type: rule.type, kql_query: extractInlineKqlQuery(rule.query, rule.language, rule.filters), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts index 28880523c774c..a15a4fcb930cd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts @@ -9,7 +9,10 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; import { parseDuration } from '@kbn/alerting-plugin/common'; -import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; +import type { + RuleMetadata, + RuleResponse, +} from '../../../../../../../common/api/detection_engine/model/rule_schema'; import type { RuleSchedule } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; @@ -18,8 +21,7 @@ export const extractRuleSchedule = (rule: RuleResponse | PrebuiltRuleAsset): Rul const from = rule.from ?? 'now-6m'; const to = rule.to ?? 'now'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ruleMeta = (rule.meta ?? {}) as any; + const ruleMeta: RuleMetadata = ('meta' in rule ? rule.meta : undefined) ?? {}; const lookbackFromMeta = String(ruleMeta.from ?? ''); const intervalDuration = parseInterval(interval); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts index 06ccf0a2b97f4..e4d9255dc2b95 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts @@ -32,6 +32,33 @@ describe('Prebuilt rule asset schema', () => { expect(result.data).toEqual(getPrebuiltRuleMock()); }); + describe('ommited fields from the rule schema are ignored', () => { + // The PrebuiltRuleAsset schema is built out of the rule schema, + // but the following fields are manually omitted. + // See: detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts + const omittedFields = [ + 'actions', + 'throttle', + 'meta', + 'output_index', + 'namespace', + 'alias_purpose', + 'alias_target_id', + 'outcome', + ]; + + test.each(omittedFields)('ignores %s since it`s an omitted field', (field) => { + const payload: Partial & Record = { + ...getPrebuiltRuleMock(), + [field]: 'some value', + }; + + const result = PrebuiltRuleAsset.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(getPrebuiltRuleMock()); + }); + }); + test('[rule_id] does not validate', () => { const payload: Partial = { rule_id: 'rule-1', @@ -64,17 +91,6 @@ describe('Prebuilt rule asset schema', () => { expect(result.data).toEqual(payload); }); - test('You can send in a namespace', () => { - const payload: PrebuiltRuleAsset = { - ...getPrebuiltRuleMock(), - namespace: 'a namespace', - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseSuccess(result); - expect(result.data).toEqual(payload); - }); - test('You can send in an empty array to threat', () => { const payload: PrebuiltRuleAsset = { ...getPrebuiltRuleMock(), @@ -449,32 +465,6 @@ describe('Prebuilt rule asset schema', () => { expect(result.data).toEqual(payload); }); - test('You can set meta to any object you want', () => { - const payload: PrebuiltRuleAsset = { - ...getPrebuiltRuleMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseSuccess(result); - expect(result.data).toEqual(payload); - }); - - test('You cannot create meta as a string', () => { - const payload: Omit & { meta: string } = { - ...getPrebuiltRuleMock(), - meta: 'should not work', - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"meta: Expected object, received string"` - ); - }); - test('validates with timeline_id and timeline_title', () => { const payload: PrebuiltRuleAsset = { ...getPrebuiltRuleMock(), @@ -500,71 +490,6 @@ describe('Prebuilt rule asset schema', () => { ); }); - test('You cannot send in an array of actions that are missing "group"', () => { - const payload: Omit = { - ...getPrebuiltRuleMock(), - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"actions.0.group: Required"`); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - const payload: Omit = { - ...getPrebuiltRuleMock(), - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"actions.0.id: Required"`); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - const payload: Omit = { - ...getPrebuiltRuleMock(), - actions: [{ group: 'group', id: 'id', params: {} }], - }; - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"actions.0.action_type_id: Required"` - ); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - const payload: Omit = { - ...getPrebuiltRuleMock(), - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"actions.0.params: Required"`); - }); - - test('You cannot send in an array of actions that are including "actionTypeId"', () => { - const payload: Omit = { - ...getPrebuiltRuleMock(), - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - - const result = PrebuiltRuleAsset.safeParse(payload); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"actions.0.action_type_id: Required"` - ); - }); - describe('note', () => { test('You can set note to a string', () => { const payload: PrebuiltRuleAsset = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts index 38ead608a3b75..056a3998e3b3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts @@ -10,9 +10,64 @@ import { RuleSignatureId, RuleVersion, BaseCreateProps, - TypeSpecificCreateProps, + EqlRuleCreateFields, + EsqlRuleCreateFields, + MachineLearningRuleCreateFields, + NewTermsRuleCreateFields, + QueryRuleCreateFields, + SavedQueryRuleCreateFields, + ThreatMatchRuleCreateFields, + ThresholdRuleCreateFields, } from '../../../../../../common/api/detection_engine/model/rule_schema'; +/** + * The PrebuiltRuleAsset schema is created based on the rule schema defined in our OpenAPI specs. + * However, we don't need all the rule schema fields to be present in the PrebuiltRuleAsset. + * We omit some of them because they are not present in https://github.com/elastic/detection-rules. + * Context: https://github.com/elastic/kibana/issues/180393 + */ +const BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor()([ + 'actions', + 'throttle', + 'meta', + 'output_index', + 'namespace', + 'alias_purpose', + 'alias_target_id', + 'outcome', +]); + +// `response_actions` is only part of the optional fields in QueryRuleCreateFields and SavedQueryRuleCreateFields +const TYPE_SPECIFIC_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor< + QueryRuleCreateFields | SavedQueryRuleCreateFields +>()(['response_actions']); + +const QueryRuleAssetFields = QueryRuleCreateFields.omit( + TYPE_SPECIFIC_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET +); +const SavedQueryRuleAssetFields = SavedQueryRuleCreateFields.omit( + TYPE_SPECIFIC_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET +); + +export const RuleAssetTypeSpecificCreateProps = z.discriminatedUnion('type', [ + EqlRuleCreateFields, + QueryRuleAssetFields, + SavedQueryRuleAssetFields, + ThresholdRuleCreateFields, + ThreatMatchRuleCreateFields, + MachineLearningRuleCreateFields, + NewTermsRuleCreateFields, + EsqlRuleCreateFields, +]); + +function zodMaskFor() { + return function (props: U[]): Record { + type PropObject = Record; + const propObjects: PropObject[] = props.map((p: U) => ({ [p]: true })); + return Object.assign({}, ...propObjects); + }; +} + /** * Asset containing source content of a prebuilt Security detection rule. * Is defined for each prebuilt rule in https://github.com/elastic/detection-rules. @@ -24,13 +79,16 @@ import { * - Data Exfiltration Detection * * Big differences between this schema and RuleCreateProps: - * - rule_id is required here - * - version is a required field that must exist + * - rule_id is a required field + * - version is a required field + * - some fields are omitted because they are not present in https://github.com/elastic/detection-rules */ export type PrebuiltRuleAsset = z.infer; -export const PrebuiltRuleAsset = BaseCreateProps.and(TypeSpecificCreateProps).and( - z.object({ - rule_id: RuleSignatureId, - version: RuleVersion, - }) -); +export const PrebuiltRuleAsset = BaseCreateProps.omit(BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET) + .and(RuleAssetTypeSpecificCreateProps) + .and( + z.object({ + rule_id: RuleSignatureId, + version: RuleVersion, + }) + );