mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
e5600b18b1
commit
d801f5dec3
12 changed files with 442 additions and 210 deletions
|
@ -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}}>;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 }));
|
||||
|
|
|
@ -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"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -284,7 +284,7 @@ export const BulkActionEditPayloadTimeline = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
const BulkActionEditPayloadInternal = z.union([
|
||||
export const BulkActionEditPayloadInternal = z.union([
|
||||
BulkActionEditPayloadTags,
|
||||
BulkActionEditPayloadIndexPatterns,
|
||||
BulkActionEditPayloadInvestigationFields,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue