Allow add_prepackaged_rules to change rule types (#128283)

This commit is contained in:
Marshall Main 2022-03-23 22:56:48 -07:00 committed by GitHub
parent bae7ee7f89
commit 2d12c94c2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 156 additions and 70 deletions

View file

@ -73,7 +73,7 @@ export const addPrepackedRulesRoute = (router: SecuritySolutionPluginRouter) =>
);
};
class PrepackagedRulesError extends Error {
export class PrepackagedRulesError extends Error {
public readonly statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
@ -147,10 +147,10 @@ export const createPrepackagedRules = async (
await updatePrepackagedRules(
rulesClient,
savedObjectsClient,
context.getSpaceId(),
rulesToUpdate,
signalsIndex,
ruleRegistryEnabled
ruleRegistryEnabled,
context.getRuleExecutionLog()
);
const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = {

View file

@ -12,7 +12,7 @@ import {
normalizeThresholdObject,
} from '../../../../common/detection_engine/utils';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
import { SanitizedAlert } from '../../../../../alerting/common';
import { AlertTypeParams, SanitizedAlert } from '../../../../../alerting/common';
import {
DEFAULT_INDICATOR_SOURCE_PATH,
NOTIFICATION_THROTTLE_NO_ACTIONS,
@ -20,7 +20,7 @@ import {
} from '../../../../common/constants';
import { CreateRulesOptions } from './types';
import { addTags } from './add_tags';
import { PartialFilter, RuleTypeParams } from '../types';
import { PartialFilter } from '../types';
import { transformToAlertThrottle, transformToNotifyWhen } from './utils';
export const createRules = async ({
@ -76,8 +76,12 @@ export const createRules = async ({
exceptionsList,
actions,
isRuleRegistryEnabled,
}: CreateRulesOptions): Promise<SanitizedAlert<RuleTypeParams>> => {
const rule = await rulesClient.create<RuleTypeParams>({
id,
}: CreateRulesOptions): Promise<SanitizedAlert<AlertTypeParams>> => {
const rule = await rulesClient.create<AlertTypeParams>({
options: {
id,
},
data: {
name,
tags: addTags(tags, ruleId, immutable),

View file

@ -194,6 +194,7 @@ export interface CreateRulesOptions {
actions: RuleAlertAction[];
isRuleRegistryEnabled: boolean;
namespace?: NamespaceOrUndefined;
id?: string;
}
export interface UpdateRulesOptions {

View file

@ -11,6 +11,7 @@ import { getFindResultWithSingleHit } from '../routes/__mocks__/request_response
import { updatePrepackagedRules } from './update_prepacked_rules';
import { patchRules } from './patch_rules';
import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock';
import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__';
jest.mock('./patch_rules');
@ -20,10 +21,12 @@ describe.each([
])('updatePrepackagedRules - %s', (_, isRuleRegistryEnabled) => {
let rulesClient: ReturnType<typeof rulesClientMock.create>;
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
let ruleExecutionLog: ReturnType<typeof ruleExecutionLogMock.forRoutes.create>;
beforeEach(() => {
rulesClient = rulesClientMock.create();
savedObjectsClient = savedObjectsClientMock.create();
ruleExecutionLog = ruleExecutionLogMock.forRoutes.create();
});
it('should omit actions and enabled when calling patchRules', async () => {
@ -42,10 +45,10 @@ describe.each([
await updatePrepackagedRules(
rulesClient,
savedObjectsClient,
'default',
[{ ...prepackagedRule, actions }],
outputIndex,
isRuleRegistryEnabled
isRuleRegistryEnabled,
ruleExecutionLog
);
expect(patchRules).toHaveBeenCalledWith(
@ -73,10 +76,10 @@ describe.each([
await updatePrepackagedRules(
rulesClient,
savedObjectsClient,
'default',
[{ ...prepackagedRule, ...updatedThreatParams }],
'output-index',
isRuleRegistryEnabled
isRuleRegistryEnabled,
ruleExecutionLog
);
expect(patchRules).toHaveBeenCalledWith(

View file

@ -15,6 +15,11 @@ import { readRules } from './read_rules';
import { PartialFilter } from '../types';
import { RuleParams } from '../schemas/rule_schemas';
import { legacyMigrate } from './utils';
import { deleteRules } from './delete_rules';
import { PrepackagedRulesError } from '../routes/rules/add_prepackaged_rules_route';
import { IRuleExecutionLogForRoutes } from '../rule_execution_log';
import { createRules } from './create_rules';
import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions';
/**
* Updates the prepackaged rules given a set of rules and output index.
@ -28,20 +33,20 @@ import { legacyMigrate } from './utils';
export const updatePrepackagedRules = async (
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
spaceId: string,
rules: AddPrepackagedRulesSchemaDecoded[],
outputIndex: string,
isRuleRegistryEnabled: boolean
isRuleRegistryEnabled: boolean,
ruleExecutionLog: IRuleExecutionLogForRoutes
): Promise<void> => {
const ruleChunks = chunk(MAX_RULES_TO_UPDATE_IN_PARALLEL, rules);
for (const ruleChunk of ruleChunks) {
const rulePromises = createPromises(
rulesClient,
savedObjectsClient,
spaceId,
ruleChunk,
outputIndex,
isRuleRegistryEnabled
isRuleRegistryEnabled,
ruleExecutionLog
);
await Promise.all(rulePromises);
}
@ -58,10 +63,10 @@ export const updatePrepackagedRules = async (
export const createPromises = (
rulesClient: RulesClient,
savedObjectsClient: SavedObjectsClientContract,
spaceId: string,
rules: AddPrepackagedRulesSchemaDecoded[],
outputIndex: string,
isRuleRegistryEnabled: boolean
isRuleRegistryEnabled: boolean,
ruleExecutionLog: IRuleExecutionLogForRoutes
): Array<Promise<PartialAlert<RuleParams> | null>> => {
return rules.map(async (rule) => {
const {
@ -128,58 +133,130 @@ export const createPromises = (
rule: existingRule,
});
// Note: we do not pass down enabled as we do not want to suddenly disable
// or enable rules on the user when they were not expecting it if a rule updates
return patchRules({
rulesClient,
author,
buildingBlockType,
description,
eventCategoryOverride,
falsePositives,
from,
query,
language,
license,
outputIndex,
rule: migratedRule,
savedId,
meta,
filters,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
timestampOverride,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatIndicatorPath,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
references,
version,
note,
anomalyThreshold,
enabled: undefined,
timelineId,
timelineTitle,
machineLearningJobId,
exceptionsList,
throttle,
actions: undefined,
});
if (!migratedRule) {
throw new PrepackagedRulesError(`Failed to find rule ${ruleId}`, 500);
}
// If we're trying to change the type of a prepackaged rule, we need to delete the old one
// and replace it with the new rule, keeping the enabled setting, actions, throttle, id,
// and exception lists from the old rule
if (type !== migratedRule.params.type) {
await deleteRules({
ruleId: migratedRule.id,
rulesClient,
ruleExecutionLog,
});
return (await createRules({
id: migratedRule.id,
isRuleRegistryEnabled,
rulesClient,
anomalyThreshold,
author,
buildingBlockType,
description,
enabled: migratedRule.enabled, // Enabled comes from existing rule
eventCategoryOverride,
falsePositives,
from,
immutable: true, // At the moment we force all prepackaged rules to be immutable
query,
language,
license,
machineLearningJobId,
outputIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
ruleId,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
to,
type,
threat,
threatFilters,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
threatQuery,
threatIndex,
threatIndicatorPath,
threshold,
throttle: migratedRule.throttle, // Throttle comes from the existing rule
timestampOverride,
references,
note,
version,
// The exceptions list passed in to this function has already been merged with the exceptions list of
// the existing rule
exceptionsList,
actions: migratedRule.actions.map(transformAlertToRuleAction), // Actions come from the existing rule
})) as PartialAlert<RuleParams>; // TODO: Replace AddPrepackagedRulesSchema with type specific rules schema so we can clean up these types
} else {
// Note: we do not pass down enabled as we do not want to suddenly disable
// or enable rules on the user when they were not expecting it if a rule updates
return patchRules({
rulesClient,
author,
buildingBlockType,
description,
eventCategoryOverride,
falsePositives,
from,
query,
language,
license,
outputIndex,
rule: migratedRule,
savedId,
meta,
filters,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
timestampOverride,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatIndicatorPath,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
references,
version,
note,
anomalyThreshold,
enabled: undefined,
timelineId,
timelineTitle,
machineLearningJobId,
exceptionsList,
throttle,
actions: undefined,
});
}
});
};

View file

@ -80,6 +80,7 @@ export interface RuleTypeParams extends AlertTypeParams {
query?: QueryOrUndefined;
filters?: unknown[];
maxSignals: MaxSignals;
namespace?: string;
riskScore: RiskScore;
riskScoreMapping: RiskScoreMappingOrUndefined;
ruleNameOverride: RuleNameOverrideOrUndefined;