Skip to content

Commit

Permalink
[Security Solution] Create Map of upgradable rule fields by type (#19…
Browse files Browse the repository at this point in the history
…0128)

## Summary

- Partially addresses #166376
(see step 1 of
[plan](#166376 (comment)))
- Partially addresses: #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 <[email protected]>
  • Loading branch information
jpdjere and kibanamachine authored Sep 12, 2024
1 parent e5600b1 commit d801f5d
Show file tree
Hide file tree
Showing 12 changed files with 443 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}}>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const ExceptionListItemEntryExists = z.object({
operator: ExceptionListItemEntryOperator,
});

const ExceptionListItemEntryNestedEntryItemInternal = z.union([
export const ExceptionListItemEntryNestedEntryItemInternal = z.union([
ExceptionListItemEntryMatch,
ExceptionListItemEntryMatchAny,
ExceptionListItemEntryExists,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
*/

import { z } from '@kbn/zod';

import {
AlertSuppression,
AnomalyThreshold,
EventCategoryOverride,
HistoryWindowStart,
InvestigationFields,
InvestigationGuide,
KqlQueryLanguage,
MachineLearningJobId,
MaxSignals,
NewTermsFields,
Expand All @@ -37,6 +39,7 @@ import {
ThreatIndicatorPath,
ThreatMapping,
Threshold,
ThresholdAlertSuppression,
TiebreakerField,
TimestampField,
} from '../../../../model/rule_schema';
Expand Down Expand Up @@ -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
Expand All @@ -99,13 +103,15 @@ 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>;
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>;
Expand All @@ -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.
Expand All @@ -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>;
Expand All @@ -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>;
Expand All @@ -143,13 +153,15 @@ 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>;
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>;
Expand All @@ -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(),
});

/**
Expand Down Expand Up @@ -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 }));
Loading

0 comments on commit d801f5d

Please sign in to comment.