[Security Solution] Skip isCustomized calculation when the feature flag is off (#201825)

**Resolves: https://github.com/elastic/kibana/issues/201632**

## Summary  

When the rule customization feature flag is disabled, we should always
return `isCustomized: false`, regardless of any changes introduced to a
rule. This ensures that we do not accidentally mark prebuilt rules as
customized in 8.16 with the feature flag off. For more details, refer to
the related issue: https://github.com/elastic/kibana/issues/201632

### Main Changes  

- The primary change in this PR is encapsulated in the
`calculateIsCustomized` function
- Other changes involve passing the feature flag to this function
- Added integration tests to cover all API CRUD operations that can be
performed with rules
This commit is contained in:
Dmitrii Shevchenko 2024-12-03 13:11:24 +01:00 committed by GitHub
parent 839a927a94
commit 22911c1828
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 425 additions and 36 deletions

View file

@ -76,7 +76,8 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/serverless.config.ts

View file

@ -58,7 +58,8 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/large_prebuilt_rules_package/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_disabled/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_delete/basic_license_essentials_tier/configs/ess.config.ts

View file

@ -92,14 +92,20 @@ export const bulkEditRules = async ({
params: modifiedParams,
};
const ruleResponse = convertAlertingRuleToRuleResponse(updatedRule);
let isCustomized = false;
if (ruleResponse.immutable === true) {
isCustomized = calculateIsCustomized({
baseRule: baseVersionsMap.get(ruleResponse.rule_id),
nextRule: ruleResponse,
isRuleCustomizationEnabled: experimentalFeatures.prebuiltRulesCustomizationEnabled,
});
}
const ruleSource =
ruleResponse.immutable === true
? {
type: 'external' as const,
isCustomized: calculateIsCustomized(
baseVersionsMap.get(ruleResponse.rule_id),
ruleResponse
),
isCustomized,
}
: {
type: 'internal' as const,

View file

@ -51,6 +51,7 @@ describe('DetectionRulesClient.createCustomRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -44,6 +44,7 @@ describe('DetectionRulesClient.createPrebuiltRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -30,6 +30,7 @@ describe('DetectionRulesClient.deleteRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -51,6 +51,7 @@ describe('DetectionRulesClient.importRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -32,6 +32,7 @@ describe('detectionRulesClient.importRules', () => {
rulesClient: rulesClientMock.create(),
mlAuthz: buildMlAuthz(),
savedObjectsClient: savedObjectsClientMock.create(),
isRuleCustomizationEnabled: true,
});
(checkRuleExceptionReferences as jest.Mock).mockReturnValue([[], []]);

View file

@ -50,6 +50,7 @@ describe('DetectionRulesClient.patchRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -38,6 +38,7 @@ interface DetectionRulesClientParams {
rulesClient: RulesClient;
savedObjectsClient: SavedObjectsClientContract;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
}
export const createDetectionRulesClient = ({
@ -45,6 +46,7 @@ export const createDetectionRulesClient = ({
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled,
}: DetectionRulesClientParams): IDetectionRulesClient => {
const prebuiltRuleAssetClient = createPrebuiltRuleAssetsClient(savedObjectsClient);
@ -89,6 +91,7 @@ export const createDetectionRulesClient = ({
prebuiltRuleAssetClient,
mlAuthz,
ruleUpdate,
isRuleCustomizationEnabled,
});
});
},
@ -101,6 +104,7 @@ export const createDetectionRulesClient = ({
prebuiltRuleAssetClient,
mlAuthz,
rulePatch,
isRuleCustomizationEnabled,
});
});
},
@ -119,6 +123,7 @@ export const createDetectionRulesClient = ({
ruleAsset,
mlAuthz,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
});
});
},
@ -131,6 +136,7 @@ export const createDetectionRulesClient = ({
importRulePayload: args,
mlAuthz,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
});
});
},

View file

@ -50,6 +50,7 @@ describe('DetectionRulesClient.updateRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -48,6 +48,7 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => {
rulesClient,
mlAuthz,
savedObjectsClient,
isRuleCustomizationEnabled: true,
});
});

View file

@ -37,6 +37,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -65,6 +66,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -94,6 +96,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError(
'event_category_override: Expected string, received number, tiebreaker_field: Expected string, received number, timestamp_field: Expected string, received number'
@ -119,6 +122,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError('alert_suppression.group_by: Expected array, received string');
});
@ -134,6 +138,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -154,6 +159,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError(
'threat_query: Expected string, received number, threat_indicator_path: Expected string, received number'
@ -170,6 +176,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -190,6 +197,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError(
"index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'"
@ -206,6 +214,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -226,6 +235,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError(
"index.0: Expected string, received number, language: Invalid enum value. Expected 'kuery' | 'lucene', received 'non-language'"
@ -244,6 +254,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -268,6 +279,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError('threshold.value: Expected number, received string');
});
@ -285,6 +297,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -308,6 +321,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -330,6 +344,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -354,6 +369,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -376,6 +392,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -394,6 +411,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError('anomaly_threshold: Expected number, received string');
});
@ -410,6 +428,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
@ -432,6 +451,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({
@ -450,6 +470,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
})
).rejects.toThrowError('new_terms_fields: Expected array, received string');
});
@ -472,6 +493,7 @@ describe('applyRulePatch', () => {
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled: true,
});
expect(patchedRule).toEqual(
expect.objectContaining({

View file

@ -51,6 +51,7 @@ interface ApplyRulePatchProps {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
existingRule: RuleResponse;
rulePatch: PatchRuleRequestBody;
isRuleCustomizationEnabled: boolean;
}
// eslint-disable-next-line complexity
@ -58,6 +59,7 @@ export const applyRulePatch = async ({
rulePatch,
existingRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
}: ApplyRulePatchProps): Promise<RuleResponse> => {
const typeSpecificParams = patchTypeSpecificParams(rulePatch, existingRule);
@ -122,6 +124,7 @@ export const applyRulePatch = async ({
nextRule.rule_source = await calculateRuleSource({
rule: nextRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
});
return nextRule;

View file

@ -17,12 +17,14 @@ interface ApplyRuleUpdateProps {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
existingRule: RuleResponse;
ruleUpdate: RuleUpdateProps;
isRuleCustomizationEnabled: boolean;
}
export const applyRuleUpdate = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate,
isRuleCustomizationEnabled,
}: ApplyRuleUpdateProps): Promise<RuleResponse> => {
const nextRule: RuleResponse = {
...applyRuleDefaults(ruleUpdate),
@ -46,6 +48,7 @@ export const applyRuleUpdate = async ({
nextRule.rule_source = await calculateRuleSource({
rule: nextRule,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
});
return nextRule;

View file

@ -12,10 +12,22 @@ import { calculateRuleFieldsDiff } from '../../../../../prebuilt_rules/logic/dif
import { convertRuleToDiffable } from '../../../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable';
import { convertPrebuiltRuleAssetToRuleResponse } from '../../converters/convert_prebuilt_rule_asset_to_rule_response';
export function calculateIsCustomized(
baseRule: PrebuiltRuleAsset | undefined,
nextRule: RuleResponse
) {
interface CalculateIsCustomizedArgs {
baseRule: PrebuiltRuleAsset | undefined;
nextRule: RuleResponse;
isRuleCustomizationEnabled: boolean;
}
export function calculateIsCustomized({
baseRule,
nextRule,
isRuleCustomizationEnabled,
}: CalculateIsCustomizedArgs) {
if (!isRuleCustomizationEnabled) {
// 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
return true;

View file

@ -43,6 +43,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
type: 'internal',
@ -59,6 +60,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual(
expect.objectContaining({
@ -79,6 +81,7 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual(
expect.objectContaining({
@ -101,6 +104,28 @@ describe('calculateRuleSource', () => {
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual(
expect.objectContaining({
type: 'external',
is_customized: false,
})
);
});
it('returns is_customized false when the rule is customized but customization is disabled', async () => {
const rule = getSampleRule();
rule.immutable = true;
rule.name = 'Updated name';
const baseRule = getSampleRuleAsset();
prebuiltRuleAssetClient.fetchAssetsByVersion.mockResolvedValueOnce([baseRule]);
const result = await calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled: false,
});
expect(result).toEqual(
expect.objectContaining({

View file

@ -16,11 +16,13 @@ import { calculateIsCustomized } from './calculate_is_customized';
interface CalculateRuleSourceProps {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
rule: RuleResponse;
isRuleCustomizationEnabled: boolean;
}
export async function calculateRuleSource({
prebuiltRuleAssetClient,
rule,
isRuleCustomizationEnabled,
}: CalculateRuleSourceProps): Promise<RuleSource> {
if (rule.immutable) {
// This is a prebuilt rule and, despite the name, they are not immutable. So
@ -33,7 +35,11 @@ export async function calculateRuleSource({
]);
const baseRule: PrebuiltRuleAsset | undefined = prebuiltRulesResponse.at(0);
const isCustomized = calculateIsCustomized(baseRule, rule);
const isCustomized = calculateIsCustomized({
baseRule,
nextRule: rule,
isRuleCustomizationEnabled,
});
return {
type: 'external',

View file

@ -26,6 +26,7 @@ interface ImportRuleOptions {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
importRulePayload: ImportRuleArgs;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
}
export const importRule = async ({
@ -34,6 +35,7 @@ export const importRule = async ({
importRulePayload,
prebuiltRuleAssetClient,
mlAuthz,
isRuleCustomizationEnabled,
}: ImportRuleOptions): Promise<RuleResponse> => {
const { ruleToImport, overwriteRules, overrideFields, allowMissingConnectorSecrets } =
importRulePayload;
@ -60,6 +62,7 @@ export const importRule = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate: rule,
isRuleCustomizationEnabled,
});
// applyRuleUpdate prefers the existing rule's values for `rule_source` and `immutable`, but we want to use the importing rule's calculated values
ruleWithUpdates = { ...ruleWithUpdates, ...overrideFields };

View file

@ -28,6 +28,7 @@ interface PatchRuleOptions {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
rulePatch: RulePatchProps;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
}
export const patchRule = async ({
@ -36,6 +37,7 @@ export const patchRule = async ({
prebuiltRuleAssetClient,
rulePatch,
mlAuthz,
isRuleCustomizationEnabled,
}: PatchRuleOptions): Promise<RuleResponse> => {
const { rule_id: ruleId, id } = rulePatch;
@ -58,6 +60,7 @@ export const patchRule = async ({
prebuiltRuleAssetClient,
existingRule,
rulePatch,
isRuleCustomizationEnabled,
});
const patchedInternalRule = await rulesClient.update({

View file

@ -27,6 +27,7 @@ interface UpdateRuleArguments {
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
ruleUpdate: RuleUpdateProps;
mlAuthz: MlAuthz;
isRuleCustomizationEnabled: boolean;
}
export const updateRule = async ({
@ -35,6 +36,7 @@ export const updateRule = async ({
prebuiltRuleAssetClient,
ruleUpdate,
mlAuthz,
isRuleCustomizationEnabled,
}: UpdateRuleArguments): Promise<RuleResponse> => {
const { rule_id: ruleId, id } = ruleUpdate;
@ -57,6 +59,7 @@ export const updateRule = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate,
isRuleCustomizationEnabled,
});
const updatedRule = await rulesClient.update({

View file

@ -25,12 +25,14 @@ export const upgradePrebuiltRule = async ({
ruleAsset,
mlAuthz,
prebuiltRuleAssetClient,
isRuleCustomizationEnabled,
}: {
actionsClient: ActionsClient;
rulesClient: RulesClient;
ruleAsset: PrebuiltRuleAsset;
mlAuthz: MlAuthz;
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
isRuleCustomizationEnabled: boolean;
}): Promise<RuleResponse> => {
await validateMlAuth(mlAuthz, ruleAsset.type);
@ -73,6 +75,7 @@ export const upgradePrebuiltRule = async ({
prebuiltRuleAssetClient,
existingRule,
ruleUpdate: ruleAsset,
isRuleCustomizationEnabled,
});
const updatedInternalRule = await rulesClient.update({

View file

@ -15,6 +15,7 @@ describe('calculateRuleSourceForImport', () => {
rule: getRulesSchemaMock(),
prebuiltRuleAssetsByRuleId: {},
isKnownPrebuiltRule: false,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
@ -33,6 +34,7 @@ describe('calculateRuleSourceForImport', () => {
rule,
prebuiltRuleAssetsByRuleId: {},
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
@ -53,6 +55,7 @@ describe('calculateRuleSourceForImport', () => {
rule,
prebuiltRuleAssetsByRuleId,
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
@ -73,6 +76,7 @@ describe('calculateRuleSourceForImport', () => {
rule,
prebuiltRuleAssetsByRuleId,
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({

View file

@ -28,10 +28,12 @@ export const calculateRuleSourceForImport = ({
rule,
prebuiltRuleAssetsByRuleId,
isKnownPrebuiltRule,
isRuleCustomizationEnabled,
}: {
rule: ValidatedRuleToImport;
prebuiltRuleAssetsByRuleId: Record<string, PrebuiltRuleAsset>;
isKnownPrebuiltRule: boolean;
isRuleCustomizationEnabled: boolean;
}): { ruleSource: RuleSource; immutable: boolean } => {
const assetWithMatchingVersion = prebuiltRuleAssetsByRuleId[rule.rule_id];
// We convert here so that RuleSource calculation can
@ -43,6 +45,7 @@ export const calculateRuleSourceForImport = ({
rule: ruleResponseForImport,
assetWithMatchingVersion,
isKnownPrebuiltRule,
isRuleCustomizationEnabled,
});
return {

View file

@ -15,6 +15,7 @@ describe('calculateRuleSourceFromAsset', () => {
rule: getRulesSchemaMock(),
assetWithMatchingVersion: undefined,
isKnownPrebuiltRule: false,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
@ -28,6 +29,7 @@ describe('calculateRuleSourceFromAsset', () => {
rule: ruleToImport,
assetWithMatchingVersion: undefined,
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
@ -47,6 +49,7 @@ describe('calculateRuleSourceFromAsset', () => {
// no other overwrites -> no differences
}),
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({
@ -65,6 +68,7 @@ describe('calculateRuleSourceFromAsset', () => {
name: 'Customized name', // mock a customization
}),
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
expect(result).toEqual({

View file

@ -24,10 +24,12 @@ export const calculateRuleSourceFromAsset = ({
rule,
assetWithMatchingVersion,
isKnownPrebuiltRule,
isRuleCustomizationEnabled,
}: {
rule: RuleResponse;
assetWithMatchingVersion: PrebuiltRuleAsset | undefined;
isKnownPrebuiltRule: boolean;
isRuleCustomizationEnabled: boolean;
}): RuleSource => {
if (!isKnownPrebuiltRule) {
return {
@ -38,11 +40,15 @@ export const calculateRuleSourceFromAsset = ({
if (assetWithMatchingVersion == null) {
return {
type: 'external',
is_customized: true,
is_customized: isRuleCustomizationEnabled ? true : false,
};
}
const isCustomized = calculateIsCustomized(assetWithMatchingVersion, rule);
const isCustomized = calculateIsCustomized({
baseRule: assetWithMatchingVersion,
nextRule: rule,
isRuleCustomizationEnabled,
});
return {
type: 'external',

View file

@ -138,6 +138,7 @@ describe('ruleSourceImporter', () => {
rule,
prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) },
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
});
@ -166,6 +167,7 @@ describe('ruleSourceImporter', () => {
rule,
prebuiltRuleAssetsByRuleId: { 'rule-1': expect.objectContaining({ rule_id: 'rule-1' }) },
isKnownPrebuiltRule: true,
isRuleCustomizationEnabled: true,
});
});
});

View file

@ -143,6 +143,8 @@ export class RuleSourceImporter implements IRuleSourceImporter {
rule,
prebuiltRuleAssetsByRuleId: this.matchingAssetsByRuleId,
isKnownPrebuiltRule: this.availableRuleAssetIds.has(rule.rule_id),
isRuleCustomizationEnabled:
this.config.experimentalFeatures.prebuiltRulesCustomizationEnabled,
});
}

View file

@ -152,6 +152,7 @@ export class RequestContextFactory implements IRequestContextFactory {
actionsClient,
savedObjectsClient: coreContext.savedObjects.client,
mlAuthz,
isRuleCustomizationEnabled: config.experimentalFeatures.prebuiltRulesCustomizationEnabled,
});
}),

View file

@ -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 { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../../../config/ess/config.base.trial')
);
const testConfig = {
...functionalConfig.getAll(),
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - ESS Env',
},
};
return testConfig;
}

View file

@ -0,0 +1,17 @@
/*
* 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 { createTestConfig } from '../../../../../../../config/serverless/config.base';
export default createTestConfig({
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Disabled Integration Tests - Serverless Env',
},
kbnTestServerArgs: [],
});

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../../../../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Rules Management - Prebuilt Rules - Prebuilt Rule Customization Disabled', function () {
loadTestFile(require.resolve('./is_customized_calculation'));
});
};

View file

@ -0,0 +1,218 @@
/*
* 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 expect from 'expect';
import {
BulkActionEditTypeEnum,
BulkActionTypeEnum,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { deleteAllRules } from '../../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../../ftr_provider_context';
import {
deleteAllPrebuiltRuleAssets,
createRuleAssetSavedObject,
createPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
} from '../../../../utils';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
const ruleAsset = createRuleAssetSavedObject({
rule_id: 'test-rule-id',
});
describe('@ess @serverless @skipInServerlessMKI is_customized calculation with disabled customization', () => {
beforeEach(async () => {
await deleteAllRules(supertest, log);
await deleteAllPrebuiltRuleAssets(es, log);
});
it('should set is_customized to "false" on prebuilt rule PATCH', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);
const { body: findResult } = await securitySolutionApi
.findRules({
query: {
per_page: 1,
filter: `alert.attributes.params.immutable: true`,
},
})
.expect(200);
const prebuiltRule = findResult.data[0];
// Check that the rule has been created and is not customized
expect(prebuiltRule).not.toBeNull();
expect(prebuiltRule.rule_source.is_customized).toEqual(false);
const { body } = await securitySolutionApi
.patchRule({
body: {
rule_id: 'test-rule-id',
name: 'some other rule name',
},
})
.expect(200);
// Check that the rule name has been updated and the rule is still not customized
expect(body).toEqual(
expect.objectContaining({
name: 'some other rule name',
})
);
expect(body.rule_source.is_customized).toEqual(false);
});
it('should set is_customized to "false" on prebuilt rule UPDATE', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);
const { body: findResult } = await securitySolutionApi
.findRules({
query: {
per_page: 1,
filter: `alert.attributes.params.immutable: true`,
},
})
.expect(200);
const prebuiltRule = findResult.data[0];
// Check that the rule has been created and is not customized
expect(prebuiltRule).not.toBeNull();
expect(prebuiltRule.rule_source.is_customized).toEqual(false);
const { body } = await securitySolutionApi
.updateRule({
body: {
...prebuiltRule,
id: undefined, // id together with rule_id is not allowed
name: 'some other rule name',
},
})
.expect(200);
// Check that the rule name has been updated and the rule is still not customized
expect(body).toEqual(
expect.objectContaining({
name: 'some other rule name',
})
);
expect(body.rule_source.is_customized).toEqual(false);
});
it('should not allow prebuilt rule customization on import', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);
const { body: findResult } = await securitySolutionApi
.findRules({
query: {
per_page: 1,
filter: `alert.attributes.params.immutable: true`,
},
})
.expect(200);
const prebuiltRule = findResult.data[0];
// Check that the rule has been created and is not customized
expect(prebuiltRule).not.toBeNull();
expect(prebuiltRule.rule_source.is_customized).toEqual(false);
const ruleBuffer = Buffer.from(
JSON.stringify({
...prebuiltRule,
name: 'some other rule name',
})
);
const { body } = await securitySolutionApi
.importRules({ query: {} })
.attach('file', ruleBuffer, 'rules.ndjson')
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200);
expect(body).toMatchObject({
rules_count: 1,
success: false,
success_count: 0,
errors: [
{
error: {
message: expect.stringContaining('Importing prebuilt rules is not supported'),
},
rule_id: 'test-rule-id',
},
],
});
// Check that the rule has not been customized
const { body: importedRule } = await securitySolutionApi.readRule({
query: { rule_id: prebuiltRule.rule_id },
});
expect(importedRule.rule_source.is_customized).toEqual(false);
});
it('should not allow rule customization on bulk edit', async () => {
await createPrebuiltRuleAssetSavedObjects(es, [ruleAsset]);
await installPrebuiltRules(es, supertest);
const { body: findResult } = await securitySolutionApi
.findRules({
query: {
per_page: 1,
filter: `alert.attributes.params.immutable: true`,
},
})
.expect(200);
const prebuiltRule = findResult.data[0];
// Check that the rule has been created and is not customized
expect(prebuiltRule).not.toBeNull();
expect(prebuiltRule.rule_source.is_customized).toEqual(false);
const { body: bulkResult } = await securitySolutionApi
.performRulesBulkAction({
query: {},
body: {
ids: [prebuiltRule.id],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditTypeEnum.add_tags,
value: ['test'],
},
],
},
})
.expect(500);
expect(bulkResult).toMatchObject(
expect.objectContaining({
attributes: expect.objectContaining({
summary: {
failed: 1,
skipped: 0,
succeeded: 0,
total: 1,
},
errors: [expect.objectContaining({ message: "Elastic rule can't be edited" })],
}),
})
);
// Check that the rule has not been customized
const { body: ruleAfterUpdate } = await securitySolutionApi.readRule({
query: { rule_id: prebuiltRule.rule_id },
});
expect(ruleAfterUpdate.rule_source.is_customized).toEqual(false);
});
});
};

View file

@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Integration Tests - ESS Env - Trial License',
'Rules Management - Prebuilt Rule Customization Enabled Integration Tests - ESS Env',
},
};
testConfig.kbnTestServer.serverArgs = testConfig.kbnTestServer.serverArgs.map((arg: string) => {

View file

@ -11,7 +11,7 @@ export default createTestConfig({
testFiles: [require.resolve('..')],
junit: {
reportName:
'Rules Management - Prebuilt Rule Customization Integration Tests - Serverless Env - Complete Tier',
'Rules Management - Prebuilt Rule Customization Enabled Integration Tests - Serverless Env',
},
kbnTestServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([

View file

@ -8,7 +8,7 @@
import { FtrProviderContext } from '../../../../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Rules Management - Prebuilt Rules - Prebuilt Rule Customization', function () {
describe('Rules Management - Prebuilt Rules - Prebuilt Rule Customization Enabled', function () {
loadTestFile(require.resolve('./is_customized_calculation'));
loadTestFile(require.resolve('./import_rules'));
loadTestFile(require.resolve('./rules_export'));

View file

@ -7,25 +7,21 @@
import expect from 'expect';
import { createRule, deleteAllRules } from '../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import {
createHistoricalPrebuiltRuleAssetSavedObjects,
createRuleAssetSavedObject,
deleteAllPrebuiltRuleAssets,
getCustomQueryRuleParams,
getSimpleRule,
getSimpleRuleOutput,
getCustomQueryRuleParams,
getSimpleRuleOutputWithoutRuleId,
installPrebuiltRules,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleOutputWithoutRuleId,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
createRule,
deleteAllAlerts,
} from '../../../../../../common/utils/security_solution';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
@ -37,12 +33,8 @@ export default ({ getService }: FtrProviderContext) => {
describe('@ess @serverless @serverlessQA patch_rules', () => {
describe('patch rules', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await deleteAllPrebuiltRuleAssets(es, log);
});
it('should patch a single rule property of name using a rule_id', async () => {
@ -262,10 +254,6 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('max signals', () => {
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('does NOT patch a rule when max_signals is less than 1', async () => {
await securitySolutionApi.createRule({
body: getCustomQueryRuleParams({ rule_id: 'rule-1', max_signals: 100 }),