From ee97a5f5b136a897a71c64192d6592a5b791cc06 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Thu, 2 May 2024 12:05:38 +0200 Subject: [PATCH] [Security Solution] Added the rule_source field to the rule schemas (#181581) **Resolves: https://github.com/elastic/kibana/issues/180121** **Resolves: https://github.com/elastic/kibana/issues/180122** **Resolves: https://github.com/elastic/kibana/issues/180124** ## Summary As part of the preparatory changes for the work in Milestone 3, we want to add the new `rule_source` field to the API schema. - Added `rule_source` as an **optional** property to `RuleResponse`, by introducing it as an optional property in the `ResponseFields` schema. - For now, all endpoints should return `undefined` for the `rule_source` field. - Added `rule_source` as an **optional** property to `RuleToImport`, which defines the schema of required and accepted fields when importing a rule. - For now, the new `rule_source` field should be ignored in the endpoint logic. - Added the `ruleSource` field to the `BaseRuleParams` schema, as an optional field. - Implemented a Zod transformation from `snake_case` to `camelCase` for object keys to reduce code duplication. --- package.json | 3 + .../templates/zod_operation_schema.handlebars | 9 +- ...s_upgrade_and_rollback_checks.test.ts.snap | 238 ++++++++++++++++++ .../rule_schema/common_attributes.gen.ts | 34 +++ .../rule_schema/common_attributes.schema.yaml | 39 +++ .../rule_schema/rule_response_schema.test.ts | 34 +++ .../model/rule_schema/rule_schemas.gen.ts | 2 + .../rule_schema/rule_schemas.schema.yaml | 2 + .../import_rules/rule_to_import.test.ts | 37 +++ .../import/create_rules_stream_from_ndjson.ts | 7 +- .../normalization/rule_converters.test.ts | 27 +- .../normalization/rule_converters.ts | 2 + .../rule_schema/model/rule_schemas.mock.ts | 2 +- .../rule_schema/model/rule_schemas.ts | 59 +++-- .../server/utils/object_case_converters.ts | 27 ++ yarn.lock | 59 ++++- 16 files changed, 543 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/utils/object_case_converters.ts diff --git a/package.json b/package.json index af8f58602675962..bbeefbc4c039899 100644 --- a/package.json +++ b/package.json @@ -958,6 +958,7 @@ "brace": "0.11.1", "brok": "^5.0.2", "byte-size": "^8.1.0", + "camelcase-keys": "7.0.2", "canvg": "^3.0.9", "cbor-x": "^1.3.3", "chalk": "^4.1.0", @@ -1139,6 +1140,7 @@ "seedrandom": "^3.0.5", "semver": "^7.5.4", "set-value": "^4.1.0", + "snakecase-keys": "^8.0.0", "source-map-support": "^0.5.19", "stats-lite": "^2.2.0", "strip-ansi": "^6.0.0", @@ -1153,6 +1155,7 @@ "ts-easing": "^0.2.0", "tslib": "^2.0.0", "type-detect": "^4.0.8", + "type-fest": "^4.17.0", "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.2", "unidiff": "^1.0.4", 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 5395edbcf5f25cc..8890d8ae4726154 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 @@ -17,11 +17,14 @@ import { {{/each}} {{#each components.schemas}} -{{#description}} +{{#if description}} /** - * {{{.}}} + * {{{description}}} + {{#if deprecated}} + * @deprecated + {{/if}} */ -{{/description}} +{{/if}} export type {{@key}} = z.infer; export const {{@key}} = {{> zod_schema_item}}; {{#if enum}} diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap index 74d04a8707469ff..7de7801c9fc9b29 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap @@ -6043,6 +6043,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, @@ -6508,6 +6542,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, @@ -7036,6 +7104,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, @@ -7436,6 +7538,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, @@ -7942,6 +8078,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, @@ -8620,6 +8790,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, @@ -9298,6 +9502,40 @@ Object { "ruleNameOverride": Object { "type": "string", }, + "ruleSource": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "is_customized": Object { + "type": "boolean", + }, + "type": Object { + "const": "external", + "type": "string", + }, + }, + "required": Array [ + "type", + "is_customized", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "type": Object { + "const": "internal", + "type": "string", + }, + }, + "required": Array [ + "type", + ], + "type": "object", + }, + ], + }, "setup": Object { "type": "string", }, 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 2c76c7939eac183..6b6bc018c8e5cfa 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 @@ -50,9 +50,42 @@ export const KqlQueryLanguage = z.enum(['kuery', 'lucene']); export type KqlQueryLanguageEnum = typeof KqlQueryLanguage.enum; export const KqlQueryLanguageEnum = KqlQueryLanguage.enum; +/** + * This field determines whether the rule is a prebuilt Elastic rule. It will be replaced with the `rule_source` field. + * @deprecated + */ export type IsRuleImmutable = z.infer; export const IsRuleImmutable = z.boolean(); +/** + * Determines whether an external/prebuilt rule has been customized by the user (i.e. any of its fields have been modified and diverged from the base value). + */ +export type IsExternalRuleCustomized = z.infer; +export const IsExternalRuleCustomized = z.boolean(); + +/** + * Type of rule source for internally sourced rules, i.e. created within the Kibana apps. + */ +export type InternalRuleSource = z.infer; +export const InternalRuleSource = z.object({ + type: z.literal('internal'), +}); + +/** + * Type of rule source for externally sourced rules, i.e. rules that have an external source, such as the Elastic Prebuilt rules repo. + */ +export type ExternalRuleSource = z.infer; +export const ExternalRuleSource = z.object({ + type: z.literal('external'), + is_customized: IsExternalRuleCustomized, +}); + +/** + * Discriminated union that determines whether the rule is internally sourced (created within the Kibana app) or has an external source, such as the Elastic Prebuilt rules repo. + */ +export type RuleSource = z.infer; +export const RuleSource = z.discriminatedUnion('type', [ExternalRuleSource, InternalRuleSource]); + /** * Determines whether the rule is enabled. */ @@ -155,6 +188,7 @@ export const BuildingBlockType = z.string(); /** * (deprecated) Has no effect. + * @deprecated */ export type AlertsIndex = z.infer; export const AlertsIndex = z.string(); 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 2d16ba7c971480e..fadb38ef1ce5fbd 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 @@ -42,6 +42,45 @@ components: IsRuleImmutable: type: boolean + deprecated: true + description: This field determines whether the rule is a prebuilt Elastic rule. It will be replaced with the `rule_source` field. + + IsExternalRuleCustomized: + type: boolean + description: Determines whether an external/prebuilt rule has been customized by the user (i.e. any of its fields have been modified and diverged from the base value). + + InternalRuleSource: + description: Type of rule source for internally sourced rules, i.e. created within the Kibana apps. + type: object + properties: + type: + type: string + enum: + - internal + required: + - type + + ExternalRuleSource: + description: Type of rule source for externally sourced rules, i.e. rules that have an external source, such as the Elastic Prebuilt rules repo. + type: object + properties: + type: + type: string + enum: + - external + is_customized: + $ref: '#/components/schemas/IsExternalRuleCustomized' + required: + - type + - is_customized + + RuleSource: + description: Discriminated union that determines whether the rule is internally sourced (created within the Kibana app) or has an external source, such as the Elastic Prebuilt rules repo. + discriminator: + propertyName: type + oneOf: + - $ref: '#/components/schemas/ExternalRuleSource' + - $ref: '#/components/schemas/InternalRuleSource' IsRuleEnabled: type: boolean diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts index e05aa65c4fa0d00..30fe84514e05a21 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts @@ -241,3 +241,37 @@ describe('investigation_fields', () => { ); }); }); + +describe('rule_source', () => { + test('it should validate a rule with "rule_source" set to internal', () => { + const payload = getRulesSchemaMock(); + payload.rule_source = { + type: 'internal', + }; + + const result = RuleResponse.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a rule with "rule_source" set to external', () => { + const payload = getRulesSchemaMock(); + payload.rule_source = { + type: 'external', + is_customized: true, + }; + + const result = RuleResponse.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a rule with "rule_source" set to undefined', () => { + const payload = getRulesSchemaMock(); + payload.rule_source = undefined; + + const result = RuleResponse.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); +}); 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 f557323f5beb498..fc29093779c7531 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 @@ -56,6 +56,7 @@ import { RuleObjectId, RuleSignatureId, IsRuleImmutable, + RuleSource, RelatedIntegrationArray, RequiredFieldArray, RuleQuery, @@ -156,6 +157,7 @@ export const ResponseFields = z.object({ id: RuleObjectId, rule_id: RuleSignatureId, immutable: IsRuleImmutable, + rule_source: RuleSource.optional(), updated_at: z.string().datetime(), updated_by: z.string(), created_at: z.string().datetime(), diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index b1ce330377c95d3..4a2279f7a7c8d14 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -163,6 +163,8 @@ components: $ref: './common_attributes.schema.yaml#/components/schemas/RuleSignatureId' immutable: $ref: './common_attributes.schema.yaml#/components/schemas/IsRuleImmutable' + rule_source: + $ref: './common_attributes.schema.yaml#/components/schemas/RuleSource' updated_at: type: string format: date-time diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts index 2894da32593fae4..87f6f08dc0fc6bf 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts @@ -1069,4 +1069,41 @@ describe('RuleToImport', () => { expect(stringifyZodError(result.error)).toContain('data_view_id: Expected string'); }); }); + + describe('rule_source', () => { + test('it should validate a rule with "rule_source" set to internal', () => { + const payload = getImportRulesSchemaMock({ + rule_source: { + type: 'internal', + }, + }); + + const result = RuleToImport.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a rule with "rule_source" set to external', () => { + const payload = getImportRulesSchemaMock({ + rule_source: { + type: 'external', + is_customized: true, + }, + }); + + const result = RuleToImport.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('it should validate a rule with "rule_source" set to undefined', () => { + const payload = getImportRulesSchemaMock({ + rule_source: undefined, + }); + + const result = RuleToImport.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/create_rules_stream_from_ndjson.ts index a14b02df8ce712c..a22cc3d3c1f9cac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/create_rules_stream_from_ndjson.ts @@ -57,7 +57,12 @@ export const validateRules = ( return obj; } - const result = RuleToImport.safeParse(obj); + const result = RuleToImport.safeParse({ + ...obj, + // Ignore the rule source field for now. A proper handling of this field + // will be added as part of https://github.com/elastic/kibana/issues/180168 + rule_source: undefined, + }); if (!result.success) { return new BadRequestError(stringifyZodError(result.error)); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts index 00c16fdcd397752..d0a14151cabac84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.test.ts @@ -5,8 +5,13 @@ * 2.0. */ -import { patchTypeSpecificSnakeToCamel, typeSpecificCamelToSnake } from './rule_converters'; import { + commonParamsCamelToSnake, + patchTypeSpecificSnakeToCamel, + typeSpecificCamelToSnake, +} from './rule_converters'; +import { + getBaseRuleParams, getEqlRuleParams, getMlRuleParams, getNewTermsRuleParams, @@ -370,4 +375,24 @@ describe('rule_converters', () => { }); }); }); + + describe('commonParamsCamelToSnake', () => { + test('should convert rule_source params to snake case', () => { + const transformedParams = commonParamsCamelToSnake({ + ...getBaseRuleParams(), + ruleSource: { + type: 'external', + isCustomized: false, + }, + }); + expect(transformedParams).toEqual( + expect.objectContaining({ + rule_source: { + type: 'external', + is_customized: false, + }, + }) + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts index 7b7290908df7019..50a1cecce88c8a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts @@ -84,6 +84,7 @@ import { } from '../utils/utils'; import { createRuleExecutionSummary } from '../../rule_monitoring'; import type { PrebuiltRuleAsset } from '../../prebuilt_rules'; +import { convertObjectKeysToSnakeCase } from '../../../../utils/object_case_converters'; const DEFAULT_FROM = 'now-6m' as const; const DEFAULT_TO = 'now' as const; @@ -691,6 +692,7 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { version: params.version, exceptions_list: params.exceptionsList, immutable: params.immutable, + rule_source: convertObjectKeysToSnakeCase(params.ruleSource), related_integrations: params.relatedIntegrations ?? [], required_fields: params.requiredFields ?? [], setup: params.setup ?? '', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 06aa99b210a2785..9ebf17caf9a8591 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -23,7 +23,7 @@ import type { import type { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; import { sampleRuleGuid } from '../../rule_types/__mocks__/es_results'; -const getBaseRuleParams = (): BaseRuleParams => { +export const getBaseRuleParams = (): BaseRuleParams => { return { author: ['Elastic'], buildingBlockType: 'default', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index 9161912924f33f2..d116f6fd7120915 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as z from 'zod'; +import type { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; import type { RuleActionArrayCamel, RuleActionNotifyWhen, @@ -21,8 +21,7 @@ import type { SIGNALS_ID, THRESHOLD_RULE_TYPE_ID, } from '@kbn/securitysolution-rules'; - -import type { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; +import * as z from 'zod'; import { RuleResponseAction } from '../../../../../common/api/detection_engine'; import type { IsRuleEnabled, @@ -32,58 +31,60 @@ import type { import { AlertsIndex, AlertsIndexNamespace, + AlertSuppressionCamel, + AnomalyThreshold, BuildingBlockType, - RuleIntervalFrom, - RuleIntervalTo, + ConcurrentSearches, + DataViewId, + EventCategoryOverride, + HistoryWindowStart, + IndexPatternArray, InvestigationFields, InvestigationGuide, IsRuleImmutable, + ItemsPerSearch, + KqlQueryLanguage, MaxSignals, + NewTermsFields, RelatedIntegrationArray, RequiredFieldArray, + RiskScore, + RiskScoreMapping, RuleAuthorArray, RuleDescription, RuleExceptionList, RuleFalsePositiveArray, + RuleFilterArray, + RuleIntervalFrom, + RuleIntervalTo, RuleLicense, RuleMetadata, RuleNameOverride, + RuleQuery, RuleReferenceArray, RuleSignatureId, + RuleSource, RuleVersion, + SavedQueryId, SetupGuide, - ThreatArray, - TimelineTemplateId, - TimelineTemplateTitle, - TimestampOverride, - TimestampOverrideFallbackDisabled, - RiskScore, - RiskScoreMapping, Severity, SeverityMapping, - ConcurrentSearches, - DataViewId, - EventCategoryOverride, - IndexPatternArray, - ItemsPerSearch, - KqlQueryLanguage, - RuleFilterArray, - RuleQuery, - SavedQueryId, + ThreatArray, ThreatIndex, ThreatIndicatorPath, ThreatMapping, ThreatQuery, - TiebreakerField, - TimestampField, - AlertSuppressionCamel, ThresholdAlertSuppression, ThresholdNormalized, - AnomalyThreshold, - HistoryWindowStart, - NewTermsFields, + TiebreakerField, + TimelineTemplateId, + TimelineTemplateTitle, + TimestampField, + TimestampOverride, + TimestampOverrideFallbackDisabled, } from '../../../../../common/api/detection_engine/model/rule_schema'; import type { SERVER_APP_ID } from '../../../../../common/constants'; +import { convertObjectKeysToCamelCase } from '../../../../utils/object_case_converters'; // 8.10.x is mapped as an array of strings export type LegacyInvestigationFields = z.infer; @@ -103,6 +104,9 @@ export const InvestigationFieldsCombined = z.union([ LegacyInvestigationFields, ]); +export type RuleSourceCamelCased = z.infer; +export const RuleSourceCamelCased = RuleSource.transform(convertObjectKeysToCamelCase); + // Conversion to an interface has to be disabled for the entire file; otherwise, // the resulting union would not be assignable to Alerting's RuleParams due to a // TypeScript bug: https://github.com/microsoft/TypeScript/issues/15300 @@ -119,6 +123,7 @@ export const BaseRuleParams = z.object({ ruleId: RuleSignatureId, investigationFields: InvestigationFieldsCombined.optional(), immutable: IsRuleImmutable, + ruleSource: RuleSourceCamelCased.optional(), license: RuleLicense.optional(), outputIndex: AlertsIndex, timelineId: TimelineTemplateId.optional(), diff --git a/x-pack/plugins/security_solution/server/utils/object_case_converters.ts b/x-pack/plugins/security_solution/server/utils/object_case_converters.ts new file mode 100644 index 000000000000000..ebe9158fe9c9952 --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/object_case_converters.ts @@ -0,0 +1,27 @@ +/* + * 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 camelcaseKeys from 'camelcase-keys'; +import snakecaseKeys from 'snakecase-keys'; +import type { CamelCasedPropertiesDeep, SnakeCasedPropertiesDeep } from 'type-fest'; + +export const convertObjectKeysToCamelCase = >( + obj: T | undefined +) => { + if (obj == null) { + return obj; + } + return camelcaseKeys(obj, { deep: true }) as unknown as CamelCasedPropertiesDeep; +}; + +export const convertObjectKeysToSnakeCase = >( + obj: T | undefined +) => { + if (obj == null) { + return obj; + } + return snakecaseKeys(obj, { deep: true }) as SnakeCasedPropertiesDeep; +}; diff --git a/yarn.lock b/yarn.lock index ef3a2545bc8002c..26e8d4b59c93d1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13279,6 +13279,16 @@ camelcase-css@2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelcase-keys@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252" + integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg== + dependencies: + camelcase "^6.3.0" + map-obj "^4.1.0" + quick-lru "^5.1.1" + type-fest "^1.2.1" + camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -13301,7 +13311,7 @@ camelcase@5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -camelcase@6, camelcase@^6.0.0, camelcase@^6.2.0: +camelcase@6, camelcase@^6.0.0, camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -28118,6 +28128,15 @@ snakecase-keys@^4.0.1: map-obj "^4.1.0" snake-case "^3.0.4" +snakecase-keys@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/snakecase-keys/-/snakecase-keys-8.0.0.tgz#87da70d45529b35f3f27af8c8ef02b428e705c68" + integrity sha512-qS7XvESuFxskrmD8/O9tOMdLdV9YTuPfNLdRJIvNJKNEZtH9XVPwmZioROwl4TsVDbOFqlQ/o7pURuF8MnjYsg== + dependencies: + map-obj "^4.1.0" + snake-case "^3.0.4" + type-fest "^4.15.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -28737,7 +28756,7 @@ string-replace-loader@^2.2.0: loader-utils "^1.2.3" schema-utils "^1.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28755,6 +28774,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -28863,7 +28891,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -28884,6 +28912,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -29989,7 +30024,7 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.0.1: +type-fest@^1.0.1, type-fest@^1.2.1: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== @@ -29999,6 +30034,11 @@ type-fest@^2.13.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-fest@^4.15.0, type-fest@^4.17.0: + version "4.17.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.17.0.tgz#4c1b2c2852d2a40ba8c0236d3afc6fc68229e5bf" + integrity sha512-9flrz1zkfLRH3jO3bLflmTxryzKMxVa7841VeMgBaNQGY6vH4RCcpN/sQLB7mQQYh1GZ5utT2deypMuCy4yicw== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -31752,7 +31792,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31786,6 +31826,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"