mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Account for missing base rule versions in is_customized calculation (#213250)
**Partially addresses: https://github.com/elastic/kibana/issues/210358** ## Summary ### Editing of prebuilt rules with missing base versions **When the base version** of a currently installed prebuilt rule **is missing** among the `security-rule` asset saved objects, and the user edits this rule: - We should mark the rule as customized, only if the new rule settings are different from the current rule settings. - For example, adding a new tag should mark the rule as customized. Then, if the user removes this tag, the rule should remain to be marked as customized. This matches the current behavior. - However, if the user saves the rule without making any changes to it, it should keep its `is_customized` field as is. This is different from the current behavior. ### Importing of prebuilt rules with missing base versions **When the base version** of a prebuilt rule that is being imported **is missing** among the `security-rule` asset saved objects, and the user imports this rule: - If this rule is not installed, it should be created with `is_customized` field set to `false`. - If this rule is already installed, it should be updated. - Its `is_customized` field should be set to `true` if the rule from the import payload is not equal to the installed rule. - Its `is_customized` field should be be kept unchanged (`false` or `true`) if the rule from the import payload is equal to the installed rule.
This commit is contained in:
parent
a81e692556
commit
87e7cd94d1
15 changed files with 310 additions and 235 deletions
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const createPrebuiltRuleObjectsClient = () => {
|
||||
return {
|
||||
fetchInstalledRulesByIds: jest.fn(),
|
||||
fetchInstalledRules: jest.fn(),
|
||||
fetchInstalledRuleVersionsByIds: jest.fn(),
|
||||
fetchInstalledRuleVersions: jest.fn(),
|
||||
};
|
||||
};
|
|
@ -40,6 +40,7 @@ import {
|
|||
} from '../../../utils/utils';
|
||||
import { RULE_MANAGEMENT_IMPORT_EXPORT_SOCKET_TIMEOUT_MS } from '../../timeouts';
|
||||
import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import { createPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
|
||||
|
||||
const CHUNK_PARSED_OBJECT_SIZE = 50;
|
||||
|
||||
|
@ -86,6 +87,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
|
|||
'licensing',
|
||||
]);
|
||||
|
||||
const rulesClient = await ctx.alerting.getRulesClient();
|
||||
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
|
||||
const ruleCustomizationStatus = detectionRulesClient.getRuleCustomizationStatus();
|
||||
const actionsClient = ctx.actions.getActionsClient();
|
||||
|
@ -159,6 +161,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
|
|||
config,
|
||||
context: ctx.securitySolution,
|
||||
prebuiltRuleAssetsClient: createPrebuiltRuleAssetsClient(savedObjectsClient),
|
||||
prebuiltRuleObjectsClient: createPrebuiltRuleObjectsClient(rulesClient),
|
||||
ruleCustomizationStatus: detectionRulesClient.getRuleCustomizationStatus(),
|
||||
});
|
||||
|
||||
|
|
|
@ -70,8 +70,8 @@ export const bulkEditRules = async ({
|
|||
const result = await rulesClient.bulkEdit<RuleParams>({
|
||||
ids: rules.map((rule) => rule.id),
|
||||
operations,
|
||||
paramsModifier: async (rule) => {
|
||||
const ruleParams = rule.params;
|
||||
paramsModifier: async (currentRule) => {
|
||||
const ruleParams = currentRule.params;
|
||||
|
||||
await validateBulkEditRule({
|
||||
mlAuthz,
|
||||
|
@ -85,23 +85,23 @@ export const bulkEditRules = async ({
|
|||
paramsActions
|
||||
);
|
||||
|
||||
// Update rule source
|
||||
const updatedRule = {
|
||||
...rule,
|
||||
const nextRule = convertAlertingRuleToRuleResponse({
|
||||
...currentRule,
|
||||
params: modifiedParams,
|
||||
};
|
||||
const ruleResponse = convertAlertingRuleToRuleResponse(updatedRule);
|
||||
});
|
||||
|
||||
let isCustomized = false;
|
||||
if (ruleResponse.immutable === true) {
|
||||
if (nextRule.immutable === true) {
|
||||
isCustomized = calculateIsCustomized({
|
||||
baseRule: baseVersionsMap.get(ruleResponse.rule_id),
|
||||
nextRule: ruleResponse,
|
||||
baseRule: baseVersionsMap.get(nextRule.rule_id),
|
||||
currentRule: convertAlertingRuleToRuleResponse(currentRule),
|
||||
nextRule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
}
|
||||
|
||||
const ruleSource =
|
||||
ruleResponse.immutable === true
|
||||
nextRule.immutable === true
|
||||
? {
|
||||
type: 'external' as const,
|
||||
isCustomized,
|
||||
|
|
|
@ -123,7 +123,8 @@ export const applyRulePatch = async ({
|
|||
};
|
||||
|
||||
nextRule.rule_source = await calculateRuleSource({
|
||||
rule: nextRule,
|
||||
nextRule,
|
||||
currentRule: existingRule,
|
||||
prebuiltRuleAssetClient,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
|
|
@ -47,7 +47,8 @@ export const applyRuleUpdate = async ({
|
|||
};
|
||||
|
||||
nextRule.rule_source = await calculateRuleSource({
|
||||
rule: nextRule,
|
||||
nextRule,
|
||||
currentRule: existingRule,
|
||||
prebuiltRuleAssetClient,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
|
|
@ -19,34 +19,68 @@ import {
|
|||
interface CalculateIsCustomizedArgs {
|
||||
baseRule: PrebuiltRuleAsset | undefined;
|
||||
nextRule: RuleResponse;
|
||||
// Current rule can be undefined in case of importing a prebuilt rule that is not installed
|
||||
currentRule: RuleResponse | undefined;
|
||||
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
}
|
||||
|
||||
export function calculateIsCustomized({
|
||||
baseRule,
|
||||
nextRule,
|
||||
currentRule,
|
||||
ruleCustomizationStatus,
|
||||
}: CalculateIsCustomizedArgs) {
|
||||
if (
|
||||
ruleCustomizationStatus.customizationDisabledReason ===
|
||||
PrebuiltRulesCustomizationDisabledReason.FeatureFlag
|
||||
) {
|
||||
// We don't want to accidentally mark rules as customized when customization is disabled.
|
||||
// We don't want to accidentally mark rules as customized when customization
|
||||
// is disabled.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (baseRule == null) {
|
||||
// If the base version is missing, we consider the rule to be customized
|
||||
if (baseRule) {
|
||||
// Base version is available, so we can determine the customization status
|
||||
// by comparing the base version with the next version
|
||||
return areRulesEqual(convertPrebuiltRuleAssetToRuleResponse(baseRule), nextRule) === false;
|
||||
}
|
||||
// Base version is not available, apply a heuristic to determine the
|
||||
// customization status
|
||||
|
||||
if (currentRule == null) {
|
||||
// Current rule is not installed and base rule is not available, so we can't
|
||||
// determine if the rule is customized. Defaulting to false.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
currentRule.rule_source.type === 'external' &&
|
||||
currentRule.rule_source.is_customized === true
|
||||
) {
|
||||
// If the rule was previously customized, there's no way to determine
|
||||
// whether the customization remained or was reverted. Keeping it as
|
||||
// customized in this case.
|
||||
return true;
|
||||
}
|
||||
|
||||
const baseRuleWithDefaults = convertPrebuiltRuleAssetToRuleResponse(baseRule);
|
||||
// If the rule has not been customized before, its customization status can be
|
||||
// determined by comparing the current version with the next version.
|
||||
return areRulesEqual(currentRule, nextRule) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to determine if two rules are equal
|
||||
*
|
||||
* @param ruleA
|
||||
* @param ruleB
|
||||
* @returns true if all rule fields are equal, false otherwise
|
||||
*/
|
||||
function areRulesEqual(ruleA: RuleResponse, ruleB: RuleResponse) {
|
||||
const fieldsDiff = calculateRuleFieldsDiff({
|
||||
base_version: MissingVersion,
|
||||
current_version: convertRuleToDiffable(baseRuleWithDefaults),
|
||||
target_version: convertRuleToDiffable(nextRule),
|
||||
current_version: convertRuleToDiffable(ruleA),
|
||||
target_version: convertRuleToDiffable(ruleB),
|
||||
});
|
||||
|
||||
return Object.values(fieldsDiff).some((diff) => diff.has_update);
|
||||
return Object.values(fieldsDiff).every((diff) => diff.has_update === false);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,8 @@ describe('calculateRuleSource', () => {
|
|||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule: rule,
|
||||
currentRule: undefined,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
|
@ -65,7 +66,8 @@ describe('calculateRuleSource', () => {
|
|||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
|
@ -86,7 +88,8 @@ describe('calculateRuleSource', () => {
|
|||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
|
@ -109,7 +112,8 @@ describe('calculateRuleSource', () => {
|
|||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
|
@ -130,7 +134,8 @@ describe('calculateRuleSource', () => {
|
|||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus: {
|
||||
isRulesCustomizationEnabled: false,
|
||||
customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.FeatureFlag,
|
||||
|
@ -154,7 +159,8 @@ describe('calculateRuleSource', () => {
|
|||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus: {
|
||||
isRulesCustomizationEnabled: false,
|
||||
customizationDisabledReason: PrebuiltRulesCustomizationDisabledReason.License,
|
||||
|
@ -167,4 +173,107 @@ describe('calculateRuleSource', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('missing base versions', () => {
|
||||
it('return is_customized false when the base version and current version are missing', async () => {
|
||||
const rule = getSampleRule();
|
||||
rule.immutable = true;
|
||||
|
||||
// No base version
|
||||
prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
nextRule: rule,
|
||||
currentRule: undefined,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns is_customized true when the current version is already customized', async () => {
|
||||
const rule = getSampleRule();
|
||||
rule.immutable = true;
|
||||
rule.rule_source = {
|
||||
type: 'external',
|
||||
is_customized: true,
|
||||
};
|
||||
|
||||
// No base version
|
||||
prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'external',
|
||||
is_customized: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns is_customized false when the current version is not customized and the next version has no changes', async () => {
|
||||
const rule = getSampleRule();
|
||||
rule.immutable = true;
|
||||
rule.rule_source = {
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
};
|
||||
|
||||
// No base version
|
||||
prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
nextRule: rule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns is_customized true when the current version is not customized and the next version has changes', async () => {
|
||||
const rule = getSampleRule();
|
||||
rule.immutable = true;
|
||||
rule.rule_source = {
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
};
|
||||
|
||||
const nextRule = {
|
||||
...rule,
|
||||
name: 'Updated name',
|
||||
};
|
||||
|
||||
// No base version
|
||||
prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
nextRule,
|
||||
currentRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'external',
|
||||
is_customized: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,29 +16,32 @@ import { calculateIsCustomized } from './calculate_is_customized';
|
|||
|
||||
interface CalculateRuleSourceProps {
|
||||
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
|
||||
rule: RuleResponse;
|
||||
nextRule: RuleResponse;
|
||||
currentRule: RuleResponse | undefined;
|
||||
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
}
|
||||
|
||||
export async function calculateRuleSource({
|
||||
prebuiltRuleAssetClient,
|
||||
rule,
|
||||
nextRule,
|
||||
currentRule,
|
||||
ruleCustomizationStatus,
|
||||
}: CalculateRuleSourceProps): Promise<RuleSource> {
|
||||
if (rule.immutable) {
|
||||
if (nextRule.immutable) {
|
||||
// This is a prebuilt rule and, despite the name, they are not immutable. So
|
||||
// we need to recalculate `ruleSource.isCustomized` based on the rule's contents.
|
||||
const prebuiltRulesResponse = await prebuiltRuleAssetClient.fetchAssetsByVersion([
|
||||
{
|
||||
rule_id: rule.rule_id,
|
||||
version: rule.version,
|
||||
rule_id: nextRule.rule_id,
|
||||
version: nextRule.version,
|
||||
},
|
||||
]);
|
||||
const baseRule: PrebuiltRuleAsset | undefined = prebuiltRulesResponse.at(0);
|
||||
|
||||
const isCustomized = calculateIsCustomized({
|
||||
baseRule,
|
||||
nextRule: rule,
|
||||
nextRule,
|
||||
currentRule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
|
|||
describe('calculateRuleSourceForImport', () => {
|
||||
it('calculates as internal if no asset is found', () => {
|
||||
const result = calculateRuleSourceForImport({
|
||||
rule: getRulesSchemaMock(),
|
||||
importedRule: getRulesSchemaMock(),
|
||||
currentRule: undefined,
|
||||
prebuiltRuleAssetsByRuleId: {},
|
||||
isKnownPrebuiltRule: false,
|
||||
ruleCustomizationStatus,
|
||||
|
@ -31,12 +32,58 @@ describe('calculateRuleSourceForImport', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calculates as modified external type if an asset is found without a matching version', () => {
|
||||
it('calculates as not modified external type if an asset is found without a matching version and no current rule present', () => {
|
||||
const rule = getRulesSchemaMock();
|
||||
rule.rule_id = 'rule_id';
|
||||
|
||||
const result = calculateRuleSourceForImport({
|
||||
rule,
|
||||
importedRule: rule,
|
||||
currentRule: undefined,
|
||||
prebuiltRuleAssetsByRuleId: {},
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ruleSource: {
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
},
|
||||
immutable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates as non modified external type if an asset is found without a matching version and current rule present without changes', () => {
|
||||
const rule = getRulesSchemaMock();
|
||||
rule.rule_id = 'rule_id';
|
||||
|
||||
const result = calculateRuleSourceForImport({
|
||||
importedRule: rule,
|
||||
currentRule: rule,
|
||||
prebuiltRuleAssetsByRuleId: {},
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ruleSource: {
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
},
|
||||
immutable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates as modified external type if an asset is found without a matching version and current rule present with changes', () => {
|
||||
const rule = getRulesSchemaMock();
|
||||
rule.rule_id = 'rule_id';
|
||||
|
||||
const result = calculateRuleSourceForImport({
|
||||
importedRule: rule,
|
||||
currentRule: {
|
||||
...rule,
|
||||
name: 'new name',
|
||||
},
|
||||
prebuiltRuleAssetsByRuleId: {},
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
|
@ -57,7 +104,8 @@ describe('calculateRuleSourceForImport', () => {
|
|||
const prebuiltRuleAssetsByRuleId = { rule_id: getPrebuiltRuleMock({ rule_id: 'rule_id' }) };
|
||||
|
||||
const result = calculateRuleSourceForImport({
|
||||
rule,
|
||||
importedRule: rule,
|
||||
currentRule: undefined,
|
||||
prebuiltRuleAssetsByRuleId,
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
|
@ -78,7 +126,8 @@ describe('calculateRuleSourceForImport', () => {
|
|||
const prebuiltRuleAssetsByRuleId = { rule_id: getPrebuiltRuleMock(rule) };
|
||||
|
||||
const result = calculateRuleSourceForImport({
|
||||
rule,
|
||||
importedRule: rule,
|
||||
currentRule: undefined,
|
||||
prebuiltRuleAssetsByRuleId,
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
RuleResponse,
|
||||
RuleSource,
|
||||
ValidatedRuleToImport,
|
||||
} from '../../../../../../common/api/detection_engine';
|
||||
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import type { PrebuiltRuleAsset } from '../../../prebuilt_rules';
|
||||
import { calculateRuleSourceFromAsset } from './calculate_rule_source_from_asset';
|
||||
import { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized';
|
||||
import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_import_to_rule_response';
|
||||
|
||||
/**
|
||||
|
@ -26,31 +27,44 @@ import { convertRuleToImportToRuleResponse } from './converters/convert_rule_to_
|
|||
* @returns The calculated rule_source and immutable fields for the rule
|
||||
*/
|
||||
export const calculateRuleSourceForImport = ({
|
||||
rule,
|
||||
importedRule,
|
||||
currentRule,
|
||||
prebuiltRuleAssetsByRuleId,
|
||||
isKnownPrebuiltRule,
|
||||
ruleCustomizationStatus,
|
||||
}: {
|
||||
rule: ValidatedRuleToImport;
|
||||
importedRule: ValidatedRuleToImport;
|
||||
currentRule: RuleResponse | undefined;
|
||||
prebuiltRuleAssetsByRuleId: Record<string, PrebuiltRuleAsset>;
|
||||
isKnownPrebuiltRule: boolean;
|
||||
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
}): { ruleSource: RuleSource; immutable: boolean } => {
|
||||
const assetWithMatchingVersion = prebuiltRuleAssetsByRuleId[rule.rule_id];
|
||||
if (!isKnownPrebuiltRule) {
|
||||
return {
|
||||
ruleSource: { type: 'internal' },
|
||||
immutable: false,
|
||||
};
|
||||
}
|
||||
|
||||
const baseRule = prebuiltRuleAssetsByRuleId[importedRule.rule_id];
|
||||
// We convert here so that RuleSource calculation can
|
||||
// continue to deal only with RuleResponses. The fields missing from the
|
||||
// incoming rule are not actually needed for the calculation, but only to
|
||||
// satisfy the type system.
|
||||
const ruleResponseForImport = convertRuleToImportToRuleResponse(rule);
|
||||
const ruleSource = calculateRuleSourceFromAsset({
|
||||
rule: ruleResponseForImport,
|
||||
assetWithMatchingVersion,
|
||||
isKnownPrebuiltRule,
|
||||
const nextRule = convertRuleToImportToRuleResponse(importedRule);
|
||||
|
||||
const isCustomized = calculateIsCustomized({
|
||||
baseRule,
|
||||
nextRule,
|
||||
currentRule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
ruleSource,
|
||||
immutable: ruleSource.type === 'external',
|
||||
ruleSource: {
|
||||
type: 'external',
|
||||
is_customized: isCustomized,
|
||||
},
|
||||
immutable: true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* 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 { calculateRuleSourceFromAsset } from './calculate_rule_source_from_asset';
|
||||
import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks';
|
||||
import { getPrebuiltRuleMock } from '../../../prebuilt_rules/mocks';
|
||||
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
|
||||
const ruleCustomizationStatus: PrebuiltRulesCustomizationStatus = {
|
||||
isRulesCustomizationEnabled: true,
|
||||
};
|
||||
|
||||
describe('calculateRuleSourceFromAsset', () => {
|
||||
it('calculates as internal if no asset is found', () => {
|
||||
const result = calculateRuleSourceFromAsset({
|
||||
rule: getRulesSchemaMock(),
|
||||
assetWithMatchingVersion: undefined,
|
||||
isKnownPrebuiltRule: false,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'internal',
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates as customized external type if an asset is found matching rule_id but not version', () => {
|
||||
const ruleToImport = getRulesSchemaMock();
|
||||
const result = calculateRuleSourceFromAsset({
|
||||
rule: ruleToImport,
|
||||
assetWithMatchingVersion: undefined,
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'external',
|
||||
is_customized: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('matching rule_id and version is found', () => {
|
||||
it('calculates as customized external type if the imported rule has all fields unchanged from the asset', () => {
|
||||
const ruleToImport = getRulesSchemaMock();
|
||||
const result = calculateRuleSourceFromAsset({
|
||||
rule: getRulesSchemaMock(), // version 1
|
||||
assetWithMatchingVersion: getPrebuiltRuleMock({
|
||||
...ruleToImport,
|
||||
version: 1, // version 1 (same version as imported rule)
|
||||
// no other overwrites -> no differences
|
||||
}),
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'external',
|
||||
is_customized: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates as non-customized external type the imported rule has fields which differ from the asset', () => {
|
||||
const ruleToImport = getRulesSchemaMock();
|
||||
const result = calculateRuleSourceFromAsset({
|
||||
rule: getRulesSchemaMock(), // version 1
|
||||
assetWithMatchingVersion: getPrebuiltRuleMock({
|
||||
...ruleToImport,
|
||||
version: 1, // version 1 (same version as imported rule)
|
||||
name: 'Customized name', // mock a customization
|
||||
}),
|
||||
isKnownPrebuiltRule: true,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'external',
|
||||
is_customized: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* 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 { RuleResponse, RuleSource } from '../../../../../../common/api/detection_engine';
|
||||
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import type { PrebuiltRuleAsset } from '../../../prebuilt_rules';
|
||||
import { calculateIsCustomized } from '../detection_rules_client/mergers/rule_source/calculate_is_customized';
|
||||
|
||||
/**
|
||||
* Calculates rule_source for a rule based on two pieces of information:
|
||||
* 1. The prebuilt rule asset that matches the specified rule_id and version
|
||||
* 2. Whether a prebuilt rule with the specified rule_id is currently installed
|
||||
*
|
||||
* @param rule The rule for which rule_source is being calculated
|
||||
* @param assetWithMatchingVersion The prebuilt rule asset that matches the specified rule_id and version
|
||||
* @param isKnownPrebuiltRule Whether a prebuilt rule with the specified rule_id is currently installed
|
||||
*
|
||||
* @returns The calculated rule_source
|
||||
*/
|
||||
export const calculateRuleSourceFromAsset = ({
|
||||
rule,
|
||||
assetWithMatchingVersion,
|
||||
isKnownPrebuiltRule,
|
||||
ruleCustomizationStatus,
|
||||
}: {
|
||||
rule: RuleResponse;
|
||||
assetWithMatchingVersion: PrebuiltRuleAsset | undefined;
|
||||
isKnownPrebuiltRule: boolean;
|
||||
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
}): RuleSource => {
|
||||
if (!isKnownPrebuiltRule) {
|
||||
return {
|
||||
type: 'internal',
|
||||
};
|
||||
}
|
||||
|
||||
if (assetWithMatchingVersion == null) {
|
||||
return {
|
||||
type: 'external',
|
||||
is_customized: ruleCustomizationStatus.isRulesCustomizationEnabled ? true : false,
|
||||
};
|
||||
}
|
||||
|
||||
const isCustomized = calculateIsCustomized({
|
||||
baseRule: assetWithMatchingVersion,
|
||||
nextRule: rule,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'external',
|
||||
is_customized: isCustomized,
|
||||
};
|
||||
};
|
|
@ -10,13 +10,14 @@ import type {
|
|||
ValidatedRuleToImport,
|
||||
} from '../../../../../../../common/api/detection_engine';
|
||||
import { createPrebuiltRuleAssetsClient as createPrebuiltRuleAssetsClientMock } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';
|
||||
import { createPrebuiltRuleObjectsClient as createPrebuiltRuleObjectsClientMock } from '../../../../prebuilt_rules/logic/rule_objects/__mocks__/prebuilt_rule_objects_client';
|
||||
import { createMockConfig, requestContextMock } from '../../../../routes/__mocks__';
|
||||
import { getPrebuiltRuleMock } from '../../../../prebuilt_rules/mocks';
|
||||
import { createRuleSourceImporter } from './rule_source_importer';
|
||||
import * as calculateRuleSourceModule from '../calculate_rule_source_for_import';
|
||||
|
||||
describe('ruleSourceImporter', () => {
|
||||
let ruleAssetsClientMock: ReturnType<typeof createPrebuiltRuleAssetsClientMock>;
|
||||
let ruleObjectsClientMock: ReturnType<typeof createPrebuiltRuleObjectsClientMock>;
|
||||
let config: ReturnType<typeof createMockConfig>;
|
||||
let context: ReturnType<typeof requestContextMock.create>['securitySolution'];
|
||||
let ruleToImport: RuleToImport;
|
||||
|
@ -30,12 +31,15 @@ describe('ruleSourceImporter', () => {
|
|||
ruleAssetsClientMock.fetchLatestAssets.mockResolvedValue([{}]);
|
||||
ruleAssetsClientMock.fetchLatestVersions.mockResolvedValue([]);
|
||||
ruleAssetsClientMock.fetchAssetsByVersion.mockResolvedValue([]);
|
||||
ruleObjectsClientMock = createPrebuiltRuleObjectsClientMock();
|
||||
ruleObjectsClientMock.fetchInstalledRulesByIds.mockResolvedValue([]);
|
||||
ruleToImport = { rule_id: 'rule-1', version: 1 } as RuleToImport;
|
||||
|
||||
subject = createRuleSourceImporter({
|
||||
context,
|
||||
config,
|
||||
prebuiltRuleAssetsClient: ruleAssetsClientMock,
|
||||
prebuiltRuleObjectsClient: ruleObjectsClientMock,
|
||||
ruleCustomizationStatus: { isRulesCustomizationEnabled: true },
|
||||
});
|
||||
});
|
||||
|
@ -112,7 +116,6 @@ describe('ruleSourceImporter', () => {
|
|||
|
||||
describe('#calculateRuleSource()', () => {
|
||||
let rule: ValidatedRuleToImport;
|
||||
let calculatorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
rule = { rule_id: 'validated-rule', version: 1 } as ValidatedRuleToImport;
|
||||
|
@ -124,23 +127,6 @@ describe('ruleSourceImporter', () => {
|
|||
getPrebuiltRuleMock({ rule_id: 'rule-2' }),
|
||||
getPrebuiltRuleMock({ rule_id: 'validated-rule' }),
|
||||
]);
|
||||
calculatorSpy = jest
|
||||
.spyOn(calculateRuleSourceModule, 'calculateRuleSourceForImport')
|
||||
.mockReturnValue({ ruleSource: { type: 'internal' }, immutable: false });
|
||||
});
|
||||
|
||||
it('invokes calculateRuleSourceForImport with the correct arguments', async () => {
|
||||
await subject.setup([rule]);
|
||||
await subject.calculateRuleSource(rule);
|
||||
|
||||
expect(calculatorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(calculatorSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rule,
|
||||
prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) },
|
||||
isKnownPrebuiltRule: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error if the rule is not known to the calculator', async () => {
|
||||
|
@ -157,23 +143,5 @@ describe('ruleSourceImporter', () => {
|
|||
`"Rule validated-rule was not registered during setup."`
|
||||
);
|
||||
});
|
||||
|
||||
describe('for rules set up without a version', () => {
|
||||
it('invokes the calculator with the correct arguments', async () => {
|
||||
await subject.setup([{ ...rule, version: undefined }]);
|
||||
await subject.calculateRuleSource(rule);
|
||||
|
||||
expect(calculatorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(calculatorSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rule,
|
||||
prebuiltRuleAssetsByRuleId: {
|
||||
'rule-1': expect.objectContaining({ rule_id: 'rule-1' }),
|
||||
},
|
||||
isKnownPrebuiltRule: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import type { SecuritySolutionApiRequestHandlerContext } from '../../../../../../types';
|
||||
import type { ConfigType } from '../../../../../../config';
|
||||
import type {
|
||||
RuleResponse,
|
||||
RuleToImport,
|
||||
ValidatedRuleToImport,
|
||||
} from '../../../../../../../common/api/detection_engine';
|
||||
|
@ -24,6 +25,7 @@ import { ensureLatestRulesPackageInstalled } from '../../../../prebuilt_rules/lo
|
|||
import { calculateRuleSourceForImport } from '../calculate_rule_source_for_import';
|
||||
import type { CalculatedRuleSource, IRuleSourceImporter } from './rule_source_importer_interface';
|
||||
import type { PrebuiltRulesCustomizationStatus } from '../../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status';
|
||||
import type { IPrebuiltRuleObjectsClient } from '../../../../prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client';
|
||||
|
||||
interface RuleSpecifier {
|
||||
rule_id: string;
|
||||
|
@ -96,25 +98,30 @@ export class RuleSourceImporter implements IRuleSourceImporter {
|
|||
private context: SecuritySolutionApiRequestHandlerContext;
|
||||
private config: ConfigType;
|
||||
private ruleAssetsClient: IPrebuiltRuleAssetsClient;
|
||||
private ruleObjectsClient: IPrebuiltRuleObjectsClient;
|
||||
private ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
private latestPackagesInstalled: boolean = false;
|
||||
private matchingAssetsByRuleId: Record<string, PrebuiltRuleAsset> = {};
|
||||
private knownRules: RuleSpecifier[] = [];
|
||||
private currentRulesById: Record<string, RuleResponse> = {};
|
||||
private rulesToImport: RuleSpecifier[] = [];
|
||||
private availableRuleAssetIds: Set<string> = new Set();
|
||||
|
||||
constructor({
|
||||
config,
|
||||
context,
|
||||
prebuiltRuleAssetsClient,
|
||||
prebuiltRuleObjectsClient,
|
||||
ruleCustomizationStatus,
|
||||
}: {
|
||||
config: ConfigType;
|
||||
context: SecuritySolutionApiRequestHandlerContext;
|
||||
prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient;
|
||||
prebuiltRuleObjectsClient: IPrebuiltRuleObjectsClient;
|
||||
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
}) {
|
||||
this.config = config;
|
||||
this.ruleAssetsClient = prebuiltRuleAssetsClient;
|
||||
this.ruleObjectsClient = prebuiltRuleObjectsClient;
|
||||
this.context = context;
|
||||
this.ruleCustomizationStatus = ruleCustomizationStatus;
|
||||
}
|
||||
|
@ -130,9 +137,12 @@ export class RuleSourceImporter implements IRuleSourceImporter {
|
|||
this.latestPackagesInstalled = true;
|
||||
}
|
||||
|
||||
this.knownRules = rules.map((rule) => ({ rule_id: rule.rule_id, version: rule.version }));
|
||||
this.rulesToImport = rules.map((rule) => ({ rule_id: rule.rule_id, version: rule.version }));
|
||||
this.matchingAssetsByRuleId = await this.fetchMatchingAssetsByRuleId();
|
||||
this.availableRuleAssetIds = new Set(await this.fetchAvailableRuleAssetIds());
|
||||
this.currentRulesById = await this.fetchInstalledRulesByIds(
|
||||
this.rulesToImport.map((rule) => rule.rule_id)
|
||||
);
|
||||
}
|
||||
|
||||
public isPrebuiltRule(rule: RuleToImport): boolean {
|
||||
|
@ -145,7 +155,8 @@ export class RuleSourceImporter implements IRuleSourceImporter {
|
|||
this.validateRuleInput(rule);
|
||||
|
||||
return calculateRuleSourceForImport({
|
||||
rule,
|
||||
importedRule: rule,
|
||||
currentRule: this.currentRulesById[rule.rule_id],
|
||||
prebuiltRuleAssetsByRuleId: this.matchingAssetsByRuleId,
|
||||
isKnownPrebuiltRule: this.availableRuleAssetIds.has(rule.rule_id),
|
||||
ruleCustomizationStatus: this.ruleCustomizationStatus,
|
||||
|
@ -155,7 +166,7 @@ export class RuleSourceImporter implements IRuleSourceImporter {
|
|||
private async fetchMatchingAssetsByRuleId(): Promise<Record<string, PrebuiltRuleAsset>> {
|
||||
this.validateSetupState();
|
||||
const matchingAssets = await fetchMatchingAssets({
|
||||
rules: this.knownRules,
|
||||
rules: this.rulesToImport,
|
||||
ruleAssetsClient: this.ruleAssetsClient,
|
||||
});
|
||||
|
||||
|
@ -165,11 +176,18 @@ export class RuleSourceImporter implements IRuleSourceImporter {
|
|||
}, {});
|
||||
}
|
||||
|
||||
private async fetchInstalledRulesByIds(ruleIds: string[]): Promise<Record<string, RuleResponse>> {
|
||||
const currentRules = await this.ruleObjectsClient.fetchInstalledRulesByIds({
|
||||
ruleIds,
|
||||
});
|
||||
return Object.fromEntries(currentRules.map((rule) => [rule.rule_id, rule]));
|
||||
}
|
||||
|
||||
private async fetchAvailableRuleAssetIds(): Promise<string[]> {
|
||||
this.validateSetupState();
|
||||
|
||||
return fetchAvailableRuleAssetIds({
|
||||
rules: this.knownRules,
|
||||
rules: this.rulesToImport,
|
||||
ruleAssetsClient: this.ruleAssetsClient,
|
||||
});
|
||||
}
|
||||
|
@ -185,7 +203,7 @@ export class RuleSourceImporter implements IRuleSourceImporter {
|
|||
|
||||
private validateRuleInput(rule: RuleToImport) {
|
||||
if (
|
||||
!this.knownRules.some(
|
||||
!this.rulesToImport.some(
|
||||
(knownRule) =>
|
||||
knownRule.rule_id === rule.rule_id &&
|
||||
(knownRule.version === rule.version || knownRule.version == null)
|
||||
|
@ -200,17 +218,20 @@ export const createRuleSourceImporter = ({
|
|||
config,
|
||||
context,
|
||||
prebuiltRuleAssetsClient,
|
||||
prebuiltRuleObjectsClient,
|
||||
ruleCustomizationStatus,
|
||||
}: {
|
||||
config: ConfigType;
|
||||
context: SecuritySolutionApiRequestHandlerContext;
|
||||
prebuiltRuleAssetsClient: IPrebuiltRuleAssetsClient;
|
||||
prebuiltRuleObjectsClient: IPrebuiltRuleObjectsClient;
|
||||
ruleCustomizationStatus: PrebuiltRulesCustomizationStatus;
|
||||
}): RuleSourceImporter => {
|
||||
return new RuleSourceImporter({
|
||||
config,
|
||||
context,
|
||||
prebuiltRuleAssetsClient,
|
||||
prebuiltRuleObjectsClient,
|
||||
ruleCustomizationStatus,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -271,7 +271,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(importedRule).toMatchObject({
|
||||
rule_id: rule.rule_id,
|
||||
version: 9999,
|
||||
rule_source: { type: 'external', is_customized: true },
|
||||
rule_source: { type: 'external', is_customized: false },
|
||||
immutable: true,
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue