Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Extend upgrade perform endpoint logic #191439

Merged
merged 21 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,17 @@ export const DiffableNewTermsFields = z.object({
alert_suppression: AlertSuppression.optional(),
});

export const DiffableFieldsByTypeUnion = z.discriminatedUnion('type', [
DiffableCustomQueryFields,
DiffableSavedQueryFields,
DiffableEqlFields,
DiffableEsqlFields,
DiffableThreatMatchFields,
DiffableThresholdFields,
DiffableMachineLearningFields,
DiffableNewTermsFields,
]);

/**
* Represents a normalized rule object that is suitable for passing to the diff algorithm.
* Every top-level field of a diffable rule can be compared separately on its own.
Expand All @@ -200,18 +211,6 @@ export const DiffableNewTermsFields = z.object({
* NOTE: Every top-level field in a DiffableRule MUST BE LOGICALLY INDEPENDENT from other
* top-level fields.
*/

export const DiffableFieldsByTypeUnion = z.discriminatedUnion('type', [
DiffableCustomQueryFields,
DiffableSavedQueryFields,
DiffableEqlFields,
DiffableEsqlFields,
DiffableThreatMatchFields,
DiffableThresholdFields,
DiffableMachineLearningFields,
DiffableNewTermsFields,
]);

export type DiffableRule = z.infer<typeof DiffableRule>;
export const DiffableRule = z.intersection(DiffableCommonFields, DiffableFieldsByTypeUnion);

Expand Down Expand Up @@ -246,3 +245,22 @@ export const DiffableAllFields = DiffableCommonFields.merge(
.merge(DiffableMachineLearningFields.omit({ type: true }))
.merge(DiffableNewTermsFields.omit({ type: true }))
.merge(z.object({ type: DiffableRuleTypes }));

const getRuleTypeFields = (schema: z.ZodObject<z.ZodRawShape>): string[] =>
Object.keys(schema.shape);

const createDiffableFieldsPerRuleType = (specificFields: z.ZodObject<z.ZodRawShape>): string[] => [
...getRuleTypeFields(DiffableCommonFields),
...getRuleTypeFields(specificFields),
];

export const DIFFABLE_RULE_TYPE_FIELDS_MAP = new Map<DiffableRuleTypes, string[]>([
['query', createDiffableFieldsPerRuleType(DiffableCustomQueryFields)],
['saved_query', createDiffableFieldsPerRuleType(DiffableSavedQueryFields)],
['eql', createDiffableFieldsPerRuleType(DiffableEqlFields)],
['esql', createDiffableFieldsPerRuleType(DiffableEsqlFields)],
['threat_match', createDiffableFieldsPerRuleType(DiffableThreatMatchFields)],
['threshold', createDiffableFieldsPerRuleType(DiffableThresholdFields)],
['machine_learning', createDiffableFieldsPerRuleType(DiffableMachineLearningFields)],
['new_terms', createDiffableFieldsPerRuleType(DiffableNewTermsFields)],
]);
Comment on lines +257 to +266
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There is no benefit having a Map instead of a Record. I'd recommend defining as a readonly Record like below since it's simpler

Suggested change
export const DIFFABLE_RULE_TYPE_FIELDS_MAP = new Map<DiffableRuleTypes, string[]>([
['query', createDiffableFieldsPerRuleType(DiffableCustomQueryFields)],
['saved_query', createDiffableFieldsPerRuleType(DiffableSavedQueryFields)],
['eql', createDiffableFieldsPerRuleType(DiffableEqlFields)],
['esql', createDiffableFieldsPerRuleType(DiffableEsqlFields)],
['threat_match', createDiffableFieldsPerRuleType(DiffableThreatMatchFields)],
['threshold', createDiffableFieldsPerRuleType(DiffableThresholdFields)],
['machine_learning', createDiffableFieldsPerRuleType(DiffableMachineLearningFields)],
['new_terms', createDiffableFieldsPerRuleType(DiffableNewTermsFields)],
]);
export const DIFFABLE_RULE_TYPE_FIELDS_MAP = {
query: createDiffableFieldsPerRuleType(DiffableCustomQueryFields),
saved_query: createDiffableFieldsPerRuleType(DiffableSavedQueryFields),
eql: createDiffableFieldsPerRuleType(DiffableEqlFields),
esql: createDiffableFieldsPerRuleType(DiffableEsqlFields),
threat_match: createDiffableFieldsPerRuleType(DiffableThreatMatchFields),
threshold: createDiffableFieldsPerRuleType(DiffableThresholdFields),
machine_learning: createDiffableFieldsPerRuleType(DiffableMachineLearningFields),
new_terms: createDiffableFieldsPerRuleType(DiffableNewTermsFields),
} as const;

Copy link
Contributor Author

@jpdjere jpdjere Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there is a benefit. By having a Map with a specific type as here, when I later do:

DIFFABLE_RULE_TYPE_FIELDS_MAP.forEach((fields, ruleType) => {
  expect(() => {
    assertDiffableFieldsMatchRuleType(fields, ruleType);
  }).not.toThrow();
});

the types is maintained.

If I create it as a Record, and do:

Object.entries(DIFFABLE_RULE_TYPE_FIELDS_MAP).forEach(([ruleType, fields]) => {
  expect(() => {
    assertDiffableFieldsMatchRuleType(fields, ruleType);
  }).not.toThrow();

then the ruleType is simply a string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

vs.

image

Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,65 @@ import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen';
import { AggregatedPrebuiltRuleError, DiffableAllFields } from '../model';
import { RuleSignatureId, RuleVersion } from '../../model';

export type Mode = z.infer<typeof Mode>;
export const Mode = z.enum(['ALL_RULES', 'SPECIFIC_RULES']);
export type ModeEnum = typeof Mode.enum;
export const ModeEnum = Mode.enum;

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;

// Specific handling of special fields according to:
// https://github.com/elastic/kibana/issues/186544
export const FIELDS_TO_UPGRADE_TO_CURRENT_VERSION = [
'enabled',
'exceptions_list',
'alert_suppression',
'actions',
'throttle',
'response_actions',
'meta',
'output_index',
'namespace',
'alias_purpose',
'alias_target_id',
'outcome',
'concurrent_searches',
'items_per_search',
] as const;

export const NON_UPGRADEABLE_DIFFABLE_FIELDS = [
'type',
'rule_id',
'version',
'author',
'license',
] as const;

type NON_UPGRADEABLE_DIFFABLE_FIELDS_TO_OMIT_TYPE = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not: TO_OMIT looks unnecessary here since it describes an action.

readonly [key in (typeof NON_UPGRADEABLE_DIFFABLE_FIELDS)[number]]: true;
};

// This transformation is needed to have Zod's `omit` accept the rule fields that need to be omitted
export const DiffableFieldsToOmit = NON_UPGRADEABLE_DIFFABLE_FIELDS.reduce((acc, field) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const DiffableFieldsToOmit = NON_UPGRADEABLE_DIFFABLE_FIELDS.reduce((acc, field) => {
export const NonUpgradableDiffableFields = NON_UPGRADEABLE_DIFFABLE_FIELDS.reduce((acc, field) => {

return { ...acc, [field]: true };
}, {} as NON_UPGRADEABLE_DIFFABLE_FIELDS_TO_OMIT_TYPE);

/**
* 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 const DiffableUpgradableFields = DiffableAllFields.omit(DiffableFieldsToOmit);

export type FieldUpgradeSpecifier<T> = z.infer<
ReturnType<typeof fieldUpgradeSpecifier<z.ZodType<T>>>
>;
const fieldUpgradeSpecifier = <T extends z.ZodTypeAny>(fieldSchema: T) =>
export const fieldUpgradeSpecifier = <T extends z.ZodTypeAny>(fieldSchema: T) =>
z.discriminatedUnion('pick_version', [
z
.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { buildSiemResponse } from '../../../routes/utils';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { getRuleGroups } from '../../model/rule_groups/get_rule_groups';

export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
Expand Down Expand Up @@ -44,7 +44,7 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter
ruleObjectsClient,
});
const { currentRules, installableRules, upgradeableRules, totalAvailableRules } =
getVersionBuckets(ruleVersionsMap);
getRuleGroups(ruleVersionsMap);

const body: GetPrebuiltRulesStatusResponseBody = {
stats: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt
import { createPrebuiltRules } from '../../logic/rule_objects/create_prebuilt_rules';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { performTimelinesInstallation } from '../../logic/perform_timelines_installation';
import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants';
import { getRuleGroups } from '../../model/rule_groups/get_rule_groups';

export const performRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
Expand Down Expand Up @@ -80,7 +80,7 @@ export const performRuleInstallationRoute = (router: SecuritySolutionPluginRoute
ruleObjectsClient,
versionSpecifiers: mode === 'ALL_RULES' ? undefined : request.body.rules,
});
const { currentRules, installableRules } = getVersionBuckets(ruleVersionsMap);
const { currentRules, installableRules } = getRuleGroups(ruleVersionsMap);

// Perform all the checks we can before we start the upgrade process
if (mode === 'SPECIFIC_RULES') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { assertDiffableFieldsMatchRuleType } from './assert_diffable_fields_match_rule_type';
import { DIFFABLE_RULE_TYPE_FIELDS_MAP } from '../../../../../../common/api/detection_engine';

describe('assertDiffableFieldsMatchRuleType', () => {
describe('valid scenarios -', () => {
it('should validate all fields in DIFFABLE_RULE_TYPE_FIELDS_MAP', () => {
DIFFABLE_RULE_TYPE_FIELDS_MAP.forEach((fields, ruleType) => {
expect(() => {
assertDiffableFieldsMatchRuleType(fields, ruleType);
}).not.toThrow();
});
});

it('should not throw an error for valid upgradeable fields', () => {
expect(() => {
assertDiffableFieldsMatchRuleType(['name', 'description', 'severity'], 'query');
}).not.toThrow();
});

it('should handle valid rule type correctly', () => {
expect(() => {
assertDiffableFieldsMatchRuleType(['eql_query'], 'eql');
}).not.toThrow();
});

it('should handle empty upgradeable fields array', () => {
expect(() => {
assertDiffableFieldsMatchRuleType([], 'query');
}).not.toThrow();
});
});

describe('invalid scenarios -', () => {
it('should throw an error for invalid upgradeable fields', () => {
expect(() => {
assertDiffableFieldsMatchRuleType(['invalid_field'], 'query');
}).toThrow("invalid_field is not a valid upgradeable field for type 'query'");
});

it('should throw for incompatible rule types', () => {
expect(() => {
assertDiffableFieldsMatchRuleType(['eql_query'], 'query');
}).toThrow("eql_query is not a valid upgradeable field for type 'query'");
});

it('should throw an error for an unknown rule type', () => {
expect(() => {
// @ts-expect-error - unknown rule
assertDiffableFieldsMatchRuleType(['name'], 'unknown_type');
}).toThrow();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 type { DiffableRuleTypes } from '../../../../../../common/api/detection_engine';
import { DIFFABLE_RULE_TYPE_FIELDS_MAP } from '../../../../../../common/api/detection_engine';

/**
* Validates that the upgradeable (diffable) fields match the target rule type's diffable fields.
*
* This function is used in the rule upgrade process to ensure that the fields
* specified for upgrade in the request body are valid for the target rule type.
* It checks each upgradeable field provided in body.rule[].fields against the
* set of diffable fields for the target rule type.
*
* @param {string[]} diffableFields - An array of field names to be upgraded.
* @param {string} ruleType - A rule type (e.g., 'query', 'eql', 'machine_learning').
* @throws {Error} If an upgradeable field is not valid for the target rule type.
*
* @examples
* assertDiffableFieldsMatchRuleType(['kql_query', 'severity'], 'query');
* assertDiffableFieldsMatchRuleType(['esql_query', 'description'], 'esql');
* assertDiffableFieldsMatchRuleType(['machine_learning_job_id'], 'eql'); // throws error
*
* @see {@link DIFFABLE_RULE_TYPE_FIELDS_MAP} in diffable_rule.ts for the mapping of rule types to their diffable fields.
*/
export const assertDiffableFieldsMatchRuleType = (
diffableFields: string[],
ruleType: DiffableRuleTypes
) => {
const diffableFieldsForType = new Set(DIFFABLE_RULE_TYPE_FIELDS_MAP.get(ruleType));
for (const diffableField of diffableFields) {
if (!diffableFieldsForType.has(diffableField)) {
throw new Error(`${diffableField} is not a valid upgradeable field for type '${ruleType}'`);
}
}
};
Loading