[Security Solution] Create Map of upgradable rule fields by type (#190128)

## Summary

- Partially addresses https://github.com/elastic/kibana/issues/166376
(see step 1 of
[plan](https://github.com/elastic/kibana/issues/166376#issuecomment-2273466115))
- Partially addresses: https://github.com/elastic/kibana/issues/190597

- Creates a Map of the fields that are upgradable during the Upgrade
workflow, by type.
- Creating this Map dynamically, based of 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.
- This Map will be used as part of the `/upgrade/_perform` endpoint
handler logic to build the payload of fields that will be upgraded to
their different versions (`BASE`, `CURRENT`, `TARGET`,
`MERGED`,`RESOLVED`)
- Creates `RuleFieldsToUpgrade` Zod schema and `FieldUpgradeSpecifier`
type, part of the `/upgrade/_perform` payload, which defines which
fields can be upgraded and how.

<br>
<details>
<summary>See output:
<b>UPGRADABLE_RULES_FIELDS_BY_TYPE_MAP</b></summary>


```ts
new Map([
    [
        "eql",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "query",
            "language",
            "index",
            "data_view_id",
            "filters",
            "event_category_override",
            "tiebreaker_field",
            "timestamp_field",
            "alert_suppression"
        ]
    ],
    [
        "query",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "index",
            "data_view_id",
            "filters",
            "saved_id",
            "alert_suppression",
            "query",
            "language"
        ]
    ],
    [
        "saved_query",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "saved_id",
            "index",
            "data_view_id",
            "filters",
            "alert_suppression",
            "query",
            "language"
        ]
    ],
    [
        "threshold",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "query",
            "threshold",
            "index",
            "data_view_id",
            "filters",
            "saved_id",
            "alert_suppression",
            "language"
        ]
    ],
    [
        "threat_match",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "query",
            "threat_query",
            "threat_mapping",
            "threat_index",
            "index",
            "data_view_id",
            "filters",
            "saved_id",
            "threat_filters",
            "threat_indicator_path",
            "threat_language",
            "concurrent_searches",
            "items_per_search",
            "alert_suppression",
            "language"
        ]
    ],
    [
        "machine_learning",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "anomaly_threshold",
            "machine_learning_job_id",
            "alert_suppression"
        ]
    ],
    [
        "new_terms",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "type",
            "query",
            "new_terms_fields",
            "history_window_start",
            "index",
            "data_view_id",
            "filters",
            "alert_suppression",
            "language"
        ]
    ],
    [
        "esql",
        [
            "name",
            "description",
            "risk_score",
            "severity",
            "rule_name_override",
            "timestamp_override",
            "timestamp_override_fallback_disabled",
            "timeline_id",
            "timeline_title",
            "license",
            "note",
            "building_block_type",
            "investigation_fields",
            "version",
            "tags",
            "enabled",
            "risk_score_mapping",
            "severity_mapping",
            "interval",
            "from",
            "to",
            "exceptions_list",
            "author",
            "false_positives",
            "references",
            "max_signals",
            "threat",
            "setup",
            "related_integrations",
            "required_fields",
            "alert_suppression",
            "type",
            "language",
            "query"
        ]
    ]
])
```
</details>
<br>
### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Juan Pablo Djeredjian 2024-09-12 18:36:37 +02:00 committed by GitHub
parent e5600b18b1
commit d801f5dec3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 442 additions and 210 deletions

View file

@ -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<typeof {{@key}}Internal>;
export const {{@key}} = {{@key}}Internal as z.ZodType<{{@key}}>;

View file

@ -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,

View file

@ -597,7 +597,7 @@ export const EsqlRuleUpdateProps = SharedUpdateProps.merge(EsqlRuleCreateFields)
export type EsqlRulePatchProps = z.infer<typeof EsqlRulePatchProps>;
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<typeof TypeSpecificCreatePropsInte
export const TypeSpecificCreateProps =
TypeSpecificCreatePropsInternal as z.ZodType<TypeSpecificCreateProps>;
const TypeSpecificPatchPropsInternal = z.union([
export const TypeSpecificPatchPropsInternal = z.union([
EqlRulePatchFields,
QueryRulePatchFields,
SavedQueryRulePatchFields,
@ -627,7 +627,7 @@ export type TypeSpecificPatchProps = z.infer<typeof TypeSpecificPatchPropsIntern
export const TypeSpecificPatchProps =
TypeSpecificPatchPropsInternal as z.ZodType<TypeSpecificPatchProps>;
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<typeof TypeSpecificResponseInternal>;
export const TypeSpecificResponse = TypeSpecificResponseInternal as z.ZodType<TypeSpecificResponse>;
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<typeof RuleCreatePropsInternal>;
export const RuleCreateProps = RuleCreatePropsInternal as z.ZodType<RuleCreateProps>;
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<typeof RuleUpdatePropsInternal>;
export const RuleUpdateProps = RuleUpdatePropsInternal as z.ZodType<RuleUpdateProps>;
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<typeof RulePatchPropsInternal>;
export const RulePatchProps = RulePatchPropsInternal as z.ZodType<RulePatchProps>;
const RuleResponseInternal = z.discriminatedUnion('type', [
export const RuleResponseInternal = z.discriminatedUnion('type', [
EqlRule,
QueryRule,
SavedQueryRule,

View file

@ -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);
});
});
});

View file

@ -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<typeof DiffableSavedQueryFields>;
@ -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<typeof DiffableEqlFields>;
@ -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<typeof DiffableEsqlFields>;
export const DiffableEsqlFields = z.object({
type: z.literal('esql'),
esql_query: RuleEsqlQuery, // NOTE: new field
alert_suppression: AlertSuppression.optional(),
});
export type DiffableThreatMatchFields = z.infer<typeof DiffableThreatMatchFields>;
@ -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<typeof DiffableThresholdFields>;
@ -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<typeof DiffableMachineLearningFields>;
@ -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<typeof DiffableNewTermsFields>;
@ -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<typeof DiffableRule>;
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<typeof DiffableRuleTypes>;
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<DiffableCustomQueryFields, 'type'> &
Omit<DiffableSavedQueryFields, 'type'> &
Omit<DiffableEqlFields, 'type'> &
Omit<DiffableEsqlFields, 'type'> &
Omit<DiffableThreatMatchFields, 'type'> &
Omit<DiffableThresholdFields, 'type'> &
Omit<DiffableMachineLearningFields, 'type'> &
Omit<DiffableNewTermsFields, 'type'> &
DiffableRuleTypeField;
interface DiffableRuleTypeField {
type: DiffableRule['type'];
}
export type DiffableAllFields = z.infer<typeof DiffableAllFields>;
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 }));

View file

@ -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: [],
};
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);
});
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"`);
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"`);
});
});
});
});

View file

@ -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<typeof PickVersionValues>;
export const PickVersionValues = z.enum(['BASE', 'CURRENT', 'TARGET', 'MERGED']);
export type PickVersionValuesEnum = typeof PickVersionValues.enum;
export const PickVersionValuesEnum = PickVersionValues.enum;
const createUpgradeFieldSchema = <T extends z.ZodType>(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<typeof DiffableUpgradableFields>;
export const DiffableUpgradableFields = DiffableAllFields.omit({
type: true,
rule_id: true,
version: true,
author: true,
license: true,
});
export type FieldUpgradeSpecifier<T> = z.infer<
ReturnType<typeof fieldUpgradeSpecifier<z.ZodType<T>>>
>;
const fieldUpgradeSpecifier = <T extends z.ZodTypeAny>(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<TFields> = {
[Field in keyof TFields]?: FieldUpgradeSpecifier<TFields[Field]>;
};
export type RuleFieldsToUpgrade = FieldUpgradeSpecifiers<DiffableUpgradableFields>;
export const RuleFieldsToUpgrade = z
.object(
mapValues(DiffableUpgradableFields.shape, (fieldSchema) => {
return fieldUpgradeSpecifier(fieldSchema).optional();
})
)
.strict();
export type RuleUpgradeSpecifier = z.infer<typeof RuleUpgradeSpecifier>;
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<typeof UpgradeSpecificRulesRequest>;

View file

@ -284,7 +284,7 @@ export const BulkActionEditPayloadTimeline = z.object({
}),
});
const BulkActionEditPayloadInternal = z.union([
export const BulkActionEditPayloadInternal = z.union([
BulkActionEditPayloadTags,
BulkActionEditPayloadIndexPatterns,
BulkActionEditPayloadInvestigationFields,

View file

@ -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,
};
};

View file

@ -199,6 +199,7 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCommonFields>
timestamp_override: simpleDiffAlgorithm,
timeline_template: simpleDiffAlgorithm,
building_block: simpleDiffAlgorithm,
investigation_fields: simpleDiffAlgorithm,
};
const calculateCustomQueryFieldsDiff = (
@ -211,6 +212,7 @@ const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableCustomQue
type: simpleDiffAlgorithm,
kql_query: simpleDiffAlgorithm,
data_source: dataSourceDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateSavedQueryFieldsDiff = (
@ -223,6 +225,7 @@ const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableSavedQuery
type: simpleDiffAlgorithm,
kql_query: simpleDiffAlgorithm,
data_source: dataSourceDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateEqlFieldsDiff = (
@ -238,6 +241,7 @@ const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEqlFields> = {
event_category_override: singleLineStringDiffAlgorithm,
timestamp_field: singleLineStringDiffAlgorithm,
tiebreaker_field: singleLineStringDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateEsqlFieldsDiff = (
@ -249,6 +253,7 @@ const calculateEsqlFieldsDiff = (
const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEsqlFields> = {
type: simpleDiffAlgorithm,
esql_query: simpleDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateThreatMatchFieldsDiff = (
@ -265,6 +270,8 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThreatMat
threat_index: scalarArrayDiffAlgorithm,
threat_mapping: simpleDiffAlgorithm,
threat_indicator_path: singleLineStringDiffAlgorithm,
threat_language: simpleDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateThresholdFieldsDiff = (
@ -278,6 +285,7 @@ const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableThresholdFi
kql_query: simpleDiffAlgorithm,
data_source: dataSourceDiffAlgorithm,
threshold: simpleDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateMachineLearningFieldsDiff = (
@ -291,6 +299,7 @@ const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableMachi
type: simpleDiffAlgorithm,
machine_learning_job_id: simpleDiffAlgorithm,
anomaly_threshold: numberDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateNewTermsFieldsDiff = (
@ -305,6 +314,7 @@ const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableNewTermsFiel
data_source: dataSourceDiffAlgorithm,
new_terms_fields: scalarArrayDiffAlgorithm,
history_window_start: singleLineStringDiffAlgorithm,
alert_suppression: simpleDiffAlgorithm,
};
const calculateAllFieldsDiff = (

View file

@ -7,10 +7,23 @@
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { getListArrayMock } from '../../../../../../common/detection_engine/schemas/types/lists.mock';
import { PrebuiltRuleAsset } from './prebuilt_rule_asset';
import { PrebuiltRuleAsset, TypeSpecificFields } from './prebuilt_rule_asset';
import { getPrebuiltRuleMock, getPrebuiltThreatMatchRuleMock } from './prebuilt_rule_asset.mock';
import { TypeSpecificCreatePropsInternal } from '../../../../../../common/api/detection_engine';
describe('Prebuilt rule asset schema', () => {
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<PrebuiltRuleAsset> = {};
@ -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

View file

@ -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<T>() {
return function <U extends keyof T>(props: U[]): Record<U, true> {
type PropObject = Record<string, boolean>;
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<BaseCreateProps>(
/**
* 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<T>() {
return function <U extends keyof T>(props: U[]): Record<U, true> {
type PropObject = Record<string, boolean>;
const propObjects: PropObject[] = props.map((p: U) => ({ [p]: true }));
return Object.assign({}, ...propObjects);
};
}
const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES = zodMaskFor<QueryRuleCreateFields>()([
...TYPE_SPECIFIC_FIELDS_TO_OMIT,
]);
const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES =
zodMaskFor<SavedQueryRuleCreateFields>()([...TYPE_SPECIFIC_FIELDS_TO_OMIT]);
export type TypeSpecificFields = z.infer<typeof TypeSpecificFields>;
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<T>() {
* - some fields are omitted because they are not present in https://github.com/elastic/detection-rules
*/
export type PrebuiltRuleAsset = z.infer<typeof PrebuiltRuleAsset>;
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();