diff --git a/packages/kbn-openapi-generator/src/template_service/templates/zod_operation_schema.handlebars b/packages/kbn-openapi-generator/src/template_service/templates/zod_operation_schema.handlebars index 80ca766585c03c..759b0d9294b8a2 100644 --- a/packages/kbn-openapi-generator/src/template_service/templates/zod_operation_schema.handlebars +++ b/packages/kbn-openapi-generator/src/template_service/templates/zod_operation_schema.handlebars @@ -34,7 +34,7 @@ export const {{@key}}: z.ZodType<{{@key}}, ZodTypeDef, {{@key}}Input> = {{> zod_ {{#if (shouldCastExplicitly this)}} {{!-- We need this temporary type to infer from it below, but in the end we want to export as a casted {{@key}} type --}} {{!-- error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed. --}} -const {{@key}}Internal = {{> zod_schema_item}}; +export const {{@key}}Internal = {{> zod_schema_item}}; export type {{@key}} = z.infer; export const {{@key}} = {{@key}}Internal as z.ZodType<{{@key}}>; diff --git a/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_item_entry.gen.ts b/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_item_entry.gen.ts index 6b6a76defde58f..0b7f0233ba429d 100644 --- a/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_item_entry.gen.ts +++ b/packages/kbn-securitysolution-exceptions-common/api/model/exception_list_item_entry.gen.ts @@ -60,7 +60,7 @@ export const ExceptionListItemEntryExists = z.object({ operator: ExceptionListItemEntryOperator, }); -const ExceptionListItemEntryNestedEntryItemInternal = z.union([ +export const ExceptionListItemEntryNestedEntryItemInternal = z.union([ ExceptionListItemEntryMatch, ExceptionListItemEntryMatchAny, ExceptionListItemEntryExists, @@ -89,7 +89,7 @@ export const ExceptionListItemEntryMatchWildcard = z.object({ operator: ExceptionListItemEntryOperator, }); -const ExceptionListItemEntryInternal = z.discriminatedUnion('type', [ +export const ExceptionListItemEntryInternal = z.discriminatedUnion('type', [ ExceptionListItemEntryMatch, ExceptionListItemEntryMatchAny, ExceptionListItemEntryList, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts index c207f86639b73a..2d3dbcd3f436ff 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts @@ -597,7 +597,7 @@ export const EsqlRuleUpdateProps = SharedUpdateProps.merge(EsqlRuleCreateFields) export type EsqlRulePatchProps = z.infer; export const EsqlRulePatchProps = SharedPatchProps.merge(EsqlRulePatchFields.partial()); -const TypeSpecificCreatePropsInternal = z.discriminatedUnion('type', [ +export const TypeSpecificCreatePropsInternal = z.discriminatedUnion('type', [ EqlRuleCreateFields, QueryRuleCreateFields, SavedQueryRuleCreateFields, @@ -612,7 +612,7 @@ export type TypeSpecificCreateProps = z.infer; -const TypeSpecificPatchPropsInternal = z.union([ +export const TypeSpecificPatchPropsInternal = z.union([ EqlRulePatchFields, QueryRulePatchFields, SavedQueryRulePatchFields, @@ -627,7 +627,7 @@ export type TypeSpecificPatchProps = z.infer; -const TypeSpecificResponseInternal = z.discriminatedUnion('type', [ +export const TypeSpecificResponseInternal = z.discriminatedUnion('type', [ EqlRuleResponseFields, QueryRuleResponseFields, SavedQueryRuleResponseFields, @@ -641,7 +641,7 @@ const TypeSpecificResponseInternal = z.discriminatedUnion('type', [ export type TypeSpecificResponse = z.infer; export const TypeSpecificResponse = TypeSpecificResponseInternal as z.ZodType; -const RuleCreatePropsInternal = z.discriminatedUnion('type', [ +export const RuleCreatePropsInternal = z.discriminatedUnion('type', [ EqlRuleCreateProps, QueryRuleCreateProps, SavedQueryRuleCreateProps, @@ -655,7 +655,7 @@ const RuleCreatePropsInternal = z.discriminatedUnion('type', [ export type RuleCreateProps = z.infer; export const RuleCreateProps = RuleCreatePropsInternal as z.ZodType; -const RuleUpdatePropsInternal = z.discriminatedUnion('type', [ +export const RuleUpdatePropsInternal = z.discriminatedUnion('type', [ EqlRuleUpdateProps, QueryRuleUpdateProps, SavedQueryRuleUpdateProps, @@ -669,7 +669,7 @@ const RuleUpdatePropsInternal = z.discriminatedUnion('type', [ export type RuleUpdateProps = z.infer; export const RuleUpdateProps = RuleUpdatePropsInternal as z.ZodType; -const RulePatchPropsInternal = z.union([ +export const RulePatchPropsInternal = z.union([ EqlRulePatchProps, QueryRulePatchProps, SavedQueryRulePatchProps, @@ -683,7 +683,7 @@ const RulePatchPropsInternal = z.union([ export type RulePatchProps = z.infer; export const RulePatchProps = RulePatchPropsInternal as z.ZodType; -const RuleResponseInternal = z.discriminatedUnion('type', [ +export const RuleResponseInternal = z.discriminatedUnion('type', [ EqlRule, QueryRule, SavedQueryRule, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.test.ts new file mode 100644 index 00000000000000..dd84d769606c20 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { DiffableFieldsByTypeUnion, DiffableAllFields, DiffableRuleTypes } from './diffable_rule'; + +describe('Diffable rule schema', () => { + describe('DiffableAllFields', () => { + it('includes all possible rules types listed in the diffable rule schemas', () => { + const diffableAllFieldsRuleTypes = DiffableAllFields.shape.type.options.map((x) => x.value); + const diffableRuleTypes = DiffableRuleTypes.options.map((x) => x.value); + expect(diffableAllFieldsRuleTypes).toStrictEqual(diffableRuleTypes); + }); + }); + + describe('DiffableRule', () => { + it('includes all possible rules types listed in the diffable rule schemas', () => { + const diffableRuleTypes = DiffableFieldsByTypeUnion.options.map((x) => x.shape.type.value); + const ruleTypes = DiffableRuleTypes.options.map((x) => x.value); + expect(diffableRuleTypes).toStrictEqual(ruleTypes); + }); + }); +}); 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 0a85dddc897237..d0a4aa12533e00 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 @@ -6,12 +6,14 @@ */ import { z } from '@kbn/zod'; - import { + AlertSuppression, AnomalyThreshold, EventCategoryOverride, HistoryWindowStart, + InvestigationFields, InvestigationGuide, + KqlQueryLanguage, MachineLearningJobId, MaxSignals, NewTermsFields, @@ -37,6 +39,7 @@ import { ThreatIndicatorPath, ThreatMapping, Threshold, + ThresholdAlertSuppression, TiebreakerField, TimestampField, } from '../../../../model/rule_schema'; @@ -88,6 +91,7 @@ export const DiffableCommonFields = z.object({ max_signals: MaxSignals, // Optional fields + investigation_fields: InvestigationFields.optional(), rule_name_override: RuleNameOverrideObject.optional(), // NOTE: new field timestamp_override: TimestampOverrideObject.optional(), // NOTE: new field timeline_template: TimelineTemplateReference.optional(), // NOTE: new field @@ -99,6 +103,7 @@ export const DiffableCustomQueryFields = z.object({ type: z.literal('query'), kql_query: RuleKqlQuery, // NOTE: new field data_source: RuleDataSource.optional(), // NOTE: new field + alert_suppression: AlertSuppression.optional(), }); export type DiffableSavedQueryFields = z.infer; @@ -106,6 +111,7 @@ export const DiffableSavedQueryFields = z.object({ type: z.literal('saved_query'), kql_query: RuleKqlQuery, // NOTE: new field data_source: RuleDataSource.optional(), // NOTE: new field + alert_suppression: AlertSuppression.optional(), }); export type DiffableEqlFields = z.infer; @@ -116,6 +122,7 @@ export const DiffableEqlFields = z.object({ event_category_override: EventCategoryOverride.optional(), timestamp_field: TimestampField.optional(), tiebreaker_field: TiebreakerField.optional(), + alert_suppression: AlertSuppression.optional(), }); // this is a new type of rule, no prebuilt rules created yet. @@ -124,6 +131,7 @@ export type DiffableEsqlFields = z.infer; export const DiffableEsqlFields = z.object({ type: z.literal('esql'), esql_query: RuleEsqlQuery, // NOTE: new field + alert_suppression: AlertSuppression.optional(), }); export type DiffableThreatMatchFields = z.infer; @@ -135,6 +143,8 @@ export const DiffableThreatMatchFields = z.object({ threat_mapping: ThreatMapping, data_source: RuleDataSource.optional(), // NOTE: new field threat_indicator_path: ThreatIndicatorPath.optional(), + threat_language: KqlQueryLanguage.optional(), + alert_suppression: AlertSuppression.optional(), }); export type DiffableThresholdFields = z.infer; @@ -143,6 +153,7 @@ export const DiffableThresholdFields = z.object({ kql_query: RuleKqlQuery, // NOTE: new field threshold: Threshold, data_source: RuleDataSource.optional(), // NOTE: new field + alert_suppression: ThresholdAlertSuppression.optional(), }); export type DiffableMachineLearningFields = z.infer; @@ -150,6 +161,7 @@ export const DiffableMachineLearningFields = z.object({ type: z.literal('machine_learning'), machine_learning_job_id: MachineLearningJobId, anomaly_threshold: AnomalyThreshold, + alert_suppression: AlertSuppression.optional(), }); export type DiffableNewTermsFields = z.infer; @@ -159,6 +171,7 @@ export const DiffableNewTermsFields = z.object({ new_terms_fields: NewTermsFields, history_window_start: HistoryWindowStart, data_source: RuleDataSource.optional(), // NOTE: new field + alert_suppression: AlertSuppression.optional(), }); /** @@ -188,36 +201,48 @@ export const DiffableNewTermsFields = z.object({ * top-level fields. */ +export const DiffableFieldsByTypeUnion = z.discriminatedUnion('type', [ + DiffableCustomQueryFields, + DiffableSavedQueryFields, + DiffableEqlFields, + DiffableEsqlFields, + DiffableThreatMatchFields, + DiffableThresholdFields, + DiffableMachineLearningFields, + DiffableNewTermsFields, +]); + export type DiffableRule = z.infer; -const DiffableRule = z.intersection( - DiffableCommonFields, - z.discriminatedUnion('type', [ - DiffableCustomQueryFields, - DiffableSavedQueryFields, - DiffableEqlFields, - DiffableEsqlFields, - DiffableThreatMatchFields, - DiffableThresholdFields, - DiffableMachineLearningFields, - DiffableNewTermsFields, - ]) -); +export const DiffableRule = z.intersection(DiffableCommonFields, DiffableFieldsByTypeUnion); + +/** + * Union of all possible rule types + */ +export type DiffableRuleTypes = z.infer; +export const DiffableRuleTypes = z.union([ + DiffableCustomQueryFields.shape.type, + DiffableSavedQueryFields.shape.type, + DiffableEqlFields.shape.type, + DiffableEsqlFields.shape.type, + DiffableThreatMatchFields.shape.type, + DiffableThresholdFields.shape.type, + DiffableMachineLearningFields.shape.type, + DiffableNewTermsFields.shape.type, +]); /** * This is a merge of all fields from all rule types into a single TS type. * This is NOT a union discriminated by rule type, as DiffableRule is. */ -export type DiffableAllFields = DiffableCommonFields & - Omit & - Omit & - Omit & - Omit & - Omit & - Omit & - Omit & - Omit & - DiffableRuleTypeField; - -interface DiffableRuleTypeField { - type: DiffableRule['type']; -} +export type DiffableAllFields = z.infer; +export const DiffableAllFields = DiffableCommonFields.merge( + DiffableCustomQueryFields.omit({ type: true }) +) + .merge(DiffableSavedQueryFields.omit({ type: true })) + .merge(DiffableEqlFields.omit({ type: true })) + .merge(DiffableEsqlFields.omit({ type: true })) + .merge(DiffableThreatMatchFields.omit({ type: true })) + .merge(DiffableThresholdFields.omit({ type: true })) + .merge(DiffableMachineLearningFields.omit({ type: true })) + .merge(DiffableNewTermsFields.omit({ type: true })) + .merge(z.object({ type: DiffableRuleTypes })); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts index b58a254f9dc495..7f7a64006adbf3 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.test.ts @@ -4,15 +4,16 @@ * 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 { - PickVersionValues, RuleUpgradeSpecifier, UpgradeSpecificRulesRequest, UpgradeAllRulesRequest, PerformRuleUpgradeResponseBody, PerformRuleUpgradeRequestBody, + RuleFieldsToUpgrade, + DiffableUpgradableFields, + PickVersionValues, } from './perform_rule_upgrade_route'; describe('Perform Rule Upgrade Route Schemas', () => { @@ -38,6 +39,130 @@ describe('Perform Rule Upgrade Route Schemas', () => { }); }); + describe('RuleFieldsToUpgrade', () => { + it('contains only upgradable fields defined in the diffable rule schemas', () => { + expect(Object.keys(RuleFieldsToUpgrade.shape)).toStrictEqual( + Object.keys(DiffableUpgradableFields.shape) + ); + }); + + describe('correctly validates valid and invalid inputs', () => { + it('validates 5 valid cases: BASE, CURRENT, TARGET, MERGED, RESOLVED', () => { + const validInputs = [ + { + name: { + pick_version: 'BASE', + }, + }, + { + description: { + pick_version: 'CURRENT', + }, + }, + { + risk_score: { + pick_version: 'TARGET', + }, + }, + { + note: { + pick_version: 'MERGED', + }, + }, + { + references: { + pick_version: 'RESOLVED', + resolved_value: ['www.example.com'], + }, + }, + ]; + + validInputs.forEach((input) => { + const result = RuleFieldsToUpgrade.safeParse(input); + expectParseSuccess(result); + expect(result.data).toEqual(input); + }); + }); + + it('does not validate invalid combination of pick_version and resolved_value', () => { + const input = { + references: { + pick_version: 'MERGED', + resolved_value: ['www.example.com'], + }, + }; + const result = RuleFieldsToUpgrade.safeParse(input); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"references: Unrecognized key(s) in object: 'resolved_value'"` + ); + }); + + it('invalidates incorrect types of resolved_values provided to multiple fields', () => { + const input = { + references: { + pick_version: 'RESOLVED', + resolved_value: 'www.example.com', + }, + tags: { + pick_version: 'RESOLVED', + resolved_value: 4, + }, + }; + const result = RuleFieldsToUpgrade.safeParse(input); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"tags.resolved_value: Expected array, received number, references.resolved_value: Expected array, received string"` + ); + }); + + it('invalidates unknown fields', () => { + const input = { + unknown_field: { + pick_version: 'CURRENT', + }, + }; + const result = RuleFieldsToUpgrade.safeParse(input); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Unrecognized key(s) in object: 'unknown_field'"` + ); + }); + + it('invalidates rule fields not part of RuleFieldsToUpgrade', () => { + const input = { + type: { + pick_version: 'BASE', + }, + rule_id: { + pick_version: 'CURRENT', + }, + version: { + pick_version: 'TARGET', + }, + author: { + pick_version: 'MERGED', + }, + license: { + pick_version: 'RESOLVED', + resolved_value: 'Elastic License', + }, + concurrent_searches: { + pick_version: 'TARGET', + }, + items_per_search: { + pick_version: 'TARGET', + }, + }; + const result = RuleFieldsToUpgrade.safeParse(input); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"Unrecognized key(s) in object: 'type', 'rule_id', 'version', 'author', 'license', 'concurrent_searches', 'items_per_search'"` + ); + }); + }); + }); + describe('RuleUpgradeSpecifier', () => { const validSpecifier = { rule_id: 'rule-1', @@ -52,7 +177,7 @@ describe('Perform Rule Upgrade Route Schemas', () => { expect(result.data).toEqual(validSpecifier); }); - test('validates a valid upgrade specifier with a fields property', () => { + test('validates a valid upgrade specifier with a valid field property', () => { const specifierWithFields = { ...validSpecifier, fields: { @@ -66,6 +191,39 @@ describe('Perform Rule Upgrade Route Schemas', () => { expect(result.data).toEqual(specifierWithFields); }); + test('rejects an upgrade specifier with an invalid fields property', () => { + const specifierWithFields = { + ...validSpecifier, + fields: { + unknown_field: { + pick_version: 'CURRENT', + }, + }, + }; + const result = RuleUpgradeSpecifier.safeParse(specifierWithFields); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"fields: Unrecognized key(s) in object: 'unknown_field'"` + ); + }); + + test('rejects an upgrade specifier with a field property with an invalid type', () => { + const specifierWithFields = { + ...validSpecifier, + fields: { + name: { + pick_version: 'CURRENT', + resolved_value: 'My name', + }, + }, + }; + const result = RuleUpgradeSpecifier.safeParse(specifierWithFields); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot( + `"fields.name: Unrecognized key(s) in object: 'resolved_value'"` + ); + }); + test('rejects upgrade specifier with invalid pick_version rule_id', () => { const invalid = { ...validSpecifier, rule_id: 123 }; const result = RuleUpgradeSpecifier.safeParse(invalid); @@ -167,38 +325,38 @@ describe('Perform Rule Upgrade Route Schemas', () => { ); }); }); -}); -describe('PerformRuleUpgradeResponseBody', () => { - const validResponse = { - summary: { - total: 1, - succeeded: 1, - skipped: 0, - failed: 0, - }, - results: { - updated: [], - skipped: [], - }, - errors: [], - }; - - test('validates a correct perform rule upgrade response', () => { - const result = PerformRuleUpgradeResponseBody.safeParse(validResponse); - expectParseSuccess(result); - expect(result.data).toEqual(validResponse); - }); + describe('PerformRuleUpgradeResponseBody', () => { + const validResponse = { + summary: { + total: 1, + succeeded: 1, + skipped: 0, + failed: 0, + }, + results: { + updated: [], + skipped: [], + }, + errors: [], + }; - test('rejects missing required fields', () => { - const propsToDelete = Object.keys(validResponse); - propsToDelete.forEach((deletedProp) => { - const invalidResponse = Object.fromEntries( - Object.entries(validResponse).filter(([key]) => key !== deletedProp) - ); - const result = PerformRuleUpgradeResponseBody.safeParse(invalidResponse); - expectParseError(result); - expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"${deletedProp}: Required"`); + test('validates a correct perform rule upgrade response', () => { + const result = PerformRuleUpgradeResponseBody.safeParse(validResponse); + expectParseSuccess(result); + expect(result.data).toEqual(validResponse); + }); + + test('rejects missing required fields', () => { + const propsToDelete = Object.keys(validResponse); + propsToDelete.forEach((deletedProp) => { + const invalidResponse = Object.fromEntries( + Object.entries(validResponse).filter(([key]) => key !== deletedProp) + ); + const result = PerformRuleUpgradeResponseBody.safeParse(invalidResponse); + expectParseError(result); + expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"${deletedProp}: Required"`); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts index 0c290c9968caab..c7d3227ef03f32 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -6,77 +6,61 @@ */ import { z } from '@kbn/zod'; - -import { - RuleSignatureId, - RuleVersion, - RuleName, - RuleTagArray, - RuleDescription, - Severity, - SeverityMapping, - RiskScore, - RiskScoreMapping, - RuleReferenceArray, - RuleFalsePositiveArray, - ThreatArray, - InvestigationGuide, - SetupGuide, - RelatedIntegrationArray, - RequiredFieldArray, - MaxSignals, - BuildingBlockType, - RuleIntervalFrom, - RuleInterval, - RuleExceptionList, - RuleNameOverride, - TimestampOverride, - TimestampOverrideFallbackDisabled, - TimelineTemplateId, - TimelineTemplateTitle, - IndexPatternArray, - DataViewId, - RuleQuery, - QueryLanguage, - RuleFilterArray, - SavedQueryId, - KqlQueryLanguage, -} from '../../model/rule_schema/common_attributes.gen'; -import { - MachineLearningJobId, - AnomalyThreshold, -} from '../../model/rule_schema/specific_attributes/ml_attributes.gen'; -import { - ThreatQuery, - ThreatMapping, - ThreatIndex, - ThreatFilters, - ThreatIndicatorPath, -} from '../../model/rule_schema/specific_attributes/threat_match_attributes.gen'; -import { - NewTermsFields, - HistoryWindowStart, -} from '../../model/rule_schema/specific_attributes/new_terms_attributes.gen'; +import { mapValues } from 'lodash'; import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen'; -import { AggregatedPrebuiltRuleError } from '../model'; +import { AggregatedPrebuiltRuleError, DiffableAllFields } from '../model'; +import { RuleSignatureId, RuleVersion } from '../../model'; export type PickVersionValues = z.infer; export const PickVersionValues = z.enum(['BASE', 'CURRENT', 'TARGET', 'MERGED']); export type PickVersionValuesEnum = typeof PickVersionValues.enum; export const PickVersionValuesEnum = PickVersionValues.enum; -const createUpgradeFieldSchema = (fieldSchema: T) => - z - .discriminatedUnion('pick_version', [ - z.object({ +/** + * Fields upgradable by the /upgrade/_perform endpoint. + * Specific fields are omitted because they are not upgradeable, and + * handled under the hood by endpoint logic. + * See: https://github.com/elastic/kibana/issues/186544 + */ +export type DiffableUpgradableFields = z.infer; +export const DiffableUpgradableFields = DiffableAllFields.omit({ + type: true, + rule_id: true, + version: true, + author: true, + license: true, +}); + +export type FieldUpgradeSpecifier = z.infer< + ReturnType>> +>; +const fieldUpgradeSpecifier = (fieldSchema: T) => + z.discriminatedUnion('pick_version', [ + z + .object({ pick_version: PickVersionValues, - }), - z.object({ + }) + .strict(), + z + .object({ pick_version: z.literal('RESOLVED'), resolved_value: fieldSchema, - }), - ]) - .optional(); + }) + .strict(), + ]); + +type FieldUpgradeSpecifiers = { + [Field in keyof TFields]?: FieldUpgradeSpecifier; +}; + +export type RuleFieldsToUpgrade = FieldUpgradeSpecifiers; +export const RuleFieldsToUpgrade = z + .object( + mapValues(DiffableUpgradableFields.shape, (fieldSchema) => { + return fieldUpgradeSpecifier(fieldSchema).optional(); + }) + ) + .strict(); export type RuleUpgradeSpecifier = z.infer; export const RuleUpgradeSpecifier = z.object({ @@ -86,52 +70,7 @@ export const RuleUpgradeSpecifier = z.object({ pick_version: PickVersionValues.optional(), // Fields that can be customized during the upgrade workflow // as decided in: https://github.com/elastic/kibana/issues/186544 - fields: z - .object({ - name: createUpgradeFieldSchema(RuleName), - tags: createUpgradeFieldSchema(RuleTagArray), - description: createUpgradeFieldSchema(RuleDescription), - severity: createUpgradeFieldSchema(Severity), - severity_mapping: createUpgradeFieldSchema(SeverityMapping), - risk_score: createUpgradeFieldSchema(RiskScore), - risk_score_mapping: createUpgradeFieldSchema(RiskScoreMapping), - references: createUpgradeFieldSchema(RuleReferenceArray), - false_positives: createUpgradeFieldSchema(RuleFalsePositiveArray), - threat: createUpgradeFieldSchema(ThreatArray), - note: createUpgradeFieldSchema(InvestigationGuide), - setup: createUpgradeFieldSchema(SetupGuide), - related_integrations: createUpgradeFieldSchema(RelatedIntegrationArray), - required_fields: createUpgradeFieldSchema(RequiredFieldArray), - max_signals: createUpgradeFieldSchema(MaxSignals), - building_block_type: createUpgradeFieldSchema(BuildingBlockType), - from: createUpgradeFieldSchema(RuleIntervalFrom), - interval: createUpgradeFieldSchema(RuleInterval), - exceptions_list: createUpgradeFieldSchema(RuleExceptionList), - rule_name_override: createUpgradeFieldSchema(RuleNameOverride), - timestamp_override: createUpgradeFieldSchema(TimestampOverride), - timestamp_override_fallback_disabled: createUpgradeFieldSchema( - TimestampOverrideFallbackDisabled - ), - timeline_id: createUpgradeFieldSchema(TimelineTemplateId), - timeline_title: createUpgradeFieldSchema(TimelineTemplateTitle), - index: createUpgradeFieldSchema(IndexPatternArray), - data_view_id: createUpgradeFieldSchema(DataViewId), - query: createUpgradeFieldSchema(RuleQuery), - language: createUpgradeFieldSchema(QueryLanguage), - filters: createUpgradeFieldSchema(RuleFilterArray), - saved_id: createUpgradeFieldSchema(SavedQueryId), - machine_learning_job_id: createUpgradeFieldSchema(MachineLearningJobId), - anomaly_threshold: createUpgradeFieldSchema(AnomalyThreshold), - threat_query: createUpgradeFieldSchema(ThreatQuery), - threat_mapping: createUpgradeFieldSchema(ThreatMapping), - threat_index: createUpgradeFieldSchema(ThreatIndex), - threat_filters: createUpgradeFieldSchema(ThreatFilters), - threat_indicator_path: createUpgradeFieldSchema(ThreatIndicatorPath), - threat_language: createUpgradeFieldSchema(KqlQueryLanguage), - new_terms_fields: createUpgradeFieldSchema(NewTermsFields), - history_window_start: createUpgradeFieldSchema(HistoryWindowStart), - }) - .optional(), + fields: RuleFieldsToUpgrade.optional(), }); export type UpgradeSpecificRulesRequest = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts index 52a52442df42ff..0f5b191a0fcd93 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts @@ -284,7 +284,7 @@ export const BulkActionEditPayloadTimeline = z.object({ }), }); -const BulkActionEditPayloadInternal = z.union([ +export const BulkActionEditPayloadInternal = z.union([ BulkActionEditPayloadTags, BulkActionEditPayloadIndexPatterns, BulkActionEditPayloadInvestigationFields, diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts index f19d8b41be40bf..45b4612e83c8e9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts @@ -142,6 +142,7 @@ const extractDiffableCommonFields = ( max_signals: rule.max_signals ?? DEFAULT_MAX_SIGNALS, // --------------------- OPTIONAL FIELDS + investigation_fields: rule.investigation_fields, rule_name_override: extractRuleNameOverrideObject(rule), timestamp_override: extractTimestampOverrideObject(rule), timeline_template: extractTimelineTemplateReference(rule), @@ -156,6 +157,7 @@ const extractDiffableCustomQueryFields = ( 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, }; }; @@ -166,6 +168,7 @@ const extractDiffableSavedQueryFieldsFromRuleObject = ( 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, }; }; @@ -179,6 +182,7 @@ const extractDiffableEqlFieldsFromRuleObject = ( event_category_override: rule.event_category_override, timestamp_field: rule.timestamp_field, tiebreaker_field: rule.tiebreaker_field, + alert_suppression: rule.alert_suppression, }; }; @@ -188,6 +192,7 @@ const extractDiffableEsqlFieldsFromRuleObject = ( return { type: rule.type, esql_query: extractRuleEsqlQuery(rule.query, rule.language), + alert_suppression: rule.alert_suppression, }; }; @@ -206,6 +211,8 @@ const extractDiffableThreatMatchFieldsFromRuleObject = ( threat_index: rule.threat_index, threat_mapping: rule.threat_mapping, threat_indicator_path: rule.threat_indicator_path, + threat_language: rule.threat_language, + alert_suppression: rule.alert_suppression, }; }; @@ -217,6 +224,7 @@ const extractDiffableThresholdFieldsFromRuleObject = ( kql_query: extractRuleKqlQuery(rule.query, rule.language, rule.filters, rule.saved_id), data_source: extractRuleDataSource(rule.index, rule.data_view_id), threshold: rule.threshold, + alert_suppression: rule.alert_suppression, }; }; @@ -227,6 +235,7 @@ const extractDiffableMachineLearningFieldsFromRuleObject = ( type: rule.type, machine_learning_job_id: rule.machine_learning_job_id, anomaly_threshold: rule.anomaly_threshold, + alert_suppression: rule.alert_suppression, }; }; @@ -239,5 +248,6 @@ const extractDiffableNewTermsFieldsFromRuleObject = ( data_source: extractRuleDataSource(rule.index, rule.data_view_id), new_terms_fields: rule.new_terms_fields, history_window_start: rule.history_window_start, + alert_suppression: rule.alert_suppression, }; }; 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 e5b3ba031809a9..4f8126d3150726 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 @@ -199,6 +199,7 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor timestamp_override: simpleDiffAlgorithm, timeline_template: simpleDiffAlgorithm, building_block: simpleDiffAlgorithm, + investigation_fields: simpleDiffAlgorithm, }; const calculateCustomQueryFieldsDiff = ( @@ -211,6 +212,7 @@ const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { event_category_override: singleLineStringDiffAlgorithm, timestamp_field: singleLineStringDiffAlgorithm, tiebreaker_field: singleLineStringDiffAlgorithm, + alert_suppression: simpleDiffAlgorithm, }; const calculateEsqlFieldsDiff = ( @@ -249,6 +253,7 @@ const calculateEsqlFieldsDiff = ( const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { type: simpleDiffAlgorithm, esql_query: simpleDiffAlgorithm, + alert_suppression: simpleDiffAlgorithm, }; const calculateThreatMatchFieldsDiff = ( @@ -265,6 +270,8 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor { + it('can be of all rule types that are supported', () => { + // Check that the discriminated union TypeSpecificFields, which is used to create + // the PrebuiltRuleAsset schema, contains all the rule types that are supported. + const createPropsTypes = TypeSpecificCreatePropsInternal.options.map( + (option) => option.shape.type.value + ); + const fieldsTypes = TypeSpecificFields.options.map((option) => option.shape.type.value); + + expect(createPropsTypes).toHaveLength(fieldsTypes.length); + expect(new Set(createPropsTypes)).toEqual(new Set(fieldsTypes)); + }); + test('empty objects do not validate', () => { const payload: Partial = {}; @@ -32,7 +45,7 @@ describe('Prebuilt rule asset schema', () => { expect(result.data).toEqual(getPrebuiltRuleMock()); }); - describe('ommited fields from the rule schema are ignored', () => { + describe('omitted 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 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 fa6c78bb7a8c13..6267be09652e81 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 @@ -6,13 +6,30 @@ */ import * as z from '@kbn/zod'; +import type { IsEqual } from 'type-fest'; +import type { TypeSpecificCreateProps } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { RuleSignatureId, RuleVersion, BaseCreateProps, - TypeSpecificCreateProps, + EqlRuleCreateFields, + EsqlRuleCreateFields, + MachineLearningRuleCreateFields, + NewTermsRuleCreateFields, + QueryRuleCreateFields, + SavedQueryRuleCreateFields, + ThreatMatchRuleCreateFields, + ThresholdRuleCreateFields, } from '../../../../../../common/api/detection_engine/model/rule_schema'; +function zodMaskFor() { + return function (props: U[]): Record { + type PropObject = Record; + const propObjects: PropObject[] = props.map((p: U) => ({ [p]: true })); + return Object.assign({}, ...propObjects); + }; +} + /** * 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. @@ -32,32 +49,41 @@ const BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor( /** * Aditionally remove fields which are part only of the optional fields in the rule types that make up - * the TypeSpecificCreateProps discriminatedUnion, by using a Zod transformation which extracts out the - * necessary fields in the rules types where they exist. Fields to extract: + * the TypeSpecificCreateProps discriminatedUnion, by recreating a discriminated union of the types, but + * with the necessary fields omitted, in the types where they exist. Fields to extract: * - response_actions: from Query and SavedQuery rules */ -const TypeSpecificFields = TypeSpecificCreateProps.transform((val) => { - switch (val.type) { - case 'query': { - const { response_actions: _, ...rest } = val; - return rest; - } - case 'saved_query': { - const { response_actions: _, ...rest } = val; - return rest; - } - default: - return val; - } -}); +const TYPE_SPECIFIC_FIELDS_TO_OMIT = ['response_actions'] as const; -function zodMaskFor() { - return function (props: U[]): Record { - type PropObject = Record; - const propObjects: PropObject[] = props.map((p: U) => ({ [p]: true })); - return Object.assign({}, ...propObjects); - }; -} +const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES = zodMaskFor()([ + ...TYPE_SPECIFIC_FIELDS_TO_OMIT, +]); +const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES = + zodMaskFor()([...TYPE_SPECIFIC_FIELDS_TO_OMIT]); + +export type TypeSpecificFields = z.infer; +export const TypeSpecificFields = z.discriminatedUnion('type', [ + EqlRuleCreateFields, + QueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), + SavedQueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES), + ThresholdRuleCreateFields, + ThreatMatchRuleCreateFields, + MachineLearningRuleCreateFields, + NewTermsRuleCreateFields, + EsqlRuleCreateFields, +]); + +// Make sure the type-specific fields contain all the same rule types as the type-specific rule params. +// TS will throw a type error if the types are not equal (for example, if a new rule type is added to +// the TypeSpecificCreateProps and the new type is not reflected in TypeSpecificFields). +export const areTypesEqual: IsEqual< + typeof TypeSpecificCreateProps._type.type, + typeof TypeSpecificFields._type.type +> = true; + +export const PrebuiltAssetBaseProps = BaseCreateProps.omit( + BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET +); /** * Asset containing source content of a prebuilt Security detection rule. @@ -75,11 +101,37 @@ function zodMaskFor() { * - 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.omit(BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET) - .and(TypeSpecificFields) - .and( - z.object({ - rule_id: RuleSignatureId, - version: RuleVersion, +export const PrebuiltRuleAsset = PrebuiltAssetBaseProps.and(TypeSpecificFields).and( + z.object({ + rule_id: RuleSignatureId, + version: RuleVersion, + }) +); + +function createUpgradableRuleFieldsPayloadByType() { + const baseFields = Object.keys(PrebuiltAssetBaseProps.shape); + + return new Map( + TypeSpecificFields.options.map((option) => { + const typeName = option.shape.type.value; + const typeSpecificFieldsForType = Object.keys(option.shape); + + return [typeName, [...baseFields, ...typeSpecificFieldsForType]]; }) ); +} + +/** + * Map of the fields payloads to be passed to the `upgradePrebuiltRules()` method during the + * Upgrade workflow (`/upgrade/_perform` endpoint) by type. + * + * Creating this Map dynamically, based on BaseCreateProps and TypeSpecificFields, ensures that we don't need to: + * - manually add rule types to this Map if they are created + * - manually add or remove any fields if they are added or removed to a specific rule type + * - manually add or remove any fields if we decide that they should not be part of the upgradable fields. + * + * Notice that this Map includes, for each rule type, all fields that are part of the BaseCreateProps and all fields that + * are part of the TypeSpecificFields, including those that are not part of RuleUpgradeSpecifierFields schema, where + * the user of the /upgrade/_perform endpoint can specify which fields to upgrade during the upgrade workflow. + */ +export const UPGRADABLE_FIELDS_PAYLOAD_BY_RULE_TYPE = createUpgradableRuleFieldsPayloadByType();