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][Detections] Adds dry_run for bulk edit and UX handle for bulk edit of ML rule index #134664

Merged
merged 68 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
5646b47
init
vitaliidm Jun 17, 2022
9b2fd17
Update update.ts
vitaliidm Jun 17, 2022
d4fbae7
cleanup
vitaliidm Jun 17, 2022
8122a59
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jun 17, 2022
b484878
IMPROVEMENTS
vitaliidm Jun 17, 2022
391afa0
Merge branch 'security-solution/bulk-edit-dry-run' of https://github.…
vitaliidm Jun 17, 2022
9333df0
end 2 end journey
vitaliidm Jun 20, 2022
c732ca4
Delete bulk_dry_run.ts
vitaliidm Jun 20, 2022
5531da2
lint fixes
vitaliidm Jun 20, 2022
51a5639
fix translations
vitaliidm Jun 20, 2022
250a20e
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 20, 2022
06b6789
fix translation
vitaliidm Jun 20, 2022
8420b9c
typing improvements
vitaliidm Jun 20, 2022
7a8671a
JSDoc
vitaliidm Jun 20, 2022
c2f0b3d
split file into smaller chunks
vitaliidm Jun 20, 2022
0a06841
modify cy test
vitaliidm Jun 21, 2022
e01f413
typings && tests
vitaliidm Jun 21, 2022
32cb7e9
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 21, 2022
af0dab7
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 21, 2022
fae222f
refactoring
vitaliidm Jun 21, 2022
8e22c86
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 21, 2022
a69512a
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 21, 2022
6a36952
refactoring to use err_code
vitaliidm Jun 22, 2022
e04c297
Merge branch 'security-solution/bulk-edit-dry-run' of https://github.…
vitaliidm Jun 22, 2022
bce9616
fix filtering error
vitaliidm Jun 22, 2022
77e140b
refactoring
vitaliidm Jun 22, 2022
16a173d
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 22, 2022
a45f612
rename files
vitaliidm Jun 23, 2022
bdf785a
Merge branch 'security-solution/bulk-edit-dry-run' of https://github.…
vitaliidm Jun 23, 2022
c7b5484
add dry_run finctional tests
vitaliidm Jun 23, 2022
786218c
small refactoring
vitaliidm Jun 23, 2022
1f871c1
lint
vitaliidm Jun 23, 2022
def91de
unit tests
vitaliidm Jun 23, 2022
b471df0
more unit tests
vitaliidm Jun 23, 2022
b4321eb
improvements
vitaliidm Jun 23, 2022
6ee9cfa
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 23, 2022
914f917
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 23, 2022
270745c
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 24, 2022
21c2e14
wording
vitaliidm Jun 24, 2022
87c0449
Merge branch 'security-solution/bulk-edit-dry-run' of https://github.…
vitaliidm Jun 24, 2022
8ca2a4e
UX feedback
vitaliidm Jun 24, 2022
add473e
fix i18n key check
vitaliidm Jun 24, 2022
c33f170
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 27, 2022
aba6c88
fix typo
vitaliidm Jun 29, 2022
37b8c71
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 29, 2022
09c5546
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 30, 2022
04e0df1
remove translations
vitaliidm Jun 30, 2022
be7282c
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jun 30, 2022
20bd1a1
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jul 5, 2022
4bac379
CR: suggestions
vitaliidm Jul 7, 2022
6d3cab2
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jul 7, 2022
8c1a9aa
CR: fix failed tests
vitaliidm Jul 7, 2022
2fa98b9
Merge branch 'security-solution/bulk-edit-dry-run' of https://github.…
vitaliidm Jul 7, 2022
1aefb30
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jul 12, 2022
371e827
fix lint
vitaliidm Jul 12, 2022
9c59ded
CR: small changes
vitaliidm Jul 12, 2022
ec2c65b
fix lint issues after resolving merge conflicts
vitaliidm Jul 12, 2022
91956ee
fix another conflict issue :/
vitaliidm Jul 12, 2022
1f90368
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jul 12, 2022
165896c
CR: validate
vitaliidm Jul 12, 2022
b555f16
Merge branch 'security-solution/bulk-edit-dry-run' of https://github.…
vitaliidm Jul 12, 2022
732ade3
CR: refactoring
vitaliidm Jul 12, 2022
d06f730
fix unit tests
vitaliidm Jul 13, 2022
27d780f
cleanup
vitaliidm Jul 13, 2022
63ee6dc
cleanup
vitaliidm Jul 13, 2022
85cd6af
cleanup
vitaliidm Jul 13, 2022
5c79354
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jul 13, 2022
b77e25d
Merge branch 'main' into security-solution/bulk-edit-dry-run
vitaliidm Jul 18, 2022
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
9 changes: 9 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,12 @@ export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY =

export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =
'securitySolution.ruleDetails.ruleExecutionLog.showMetrics.v8.2';

/**
* Error codes that can be thrown during _bulk_action API dry_run call and be processed and displayed to end user
*/
export enum BulkActionsDryRunErrCode {
IMMUTABLE = 'IMMUTABLE',
MACHINE_LEARNING_AUTH = 'MACHINE_LEARNING_AUTH',
MACHINE_LEARNING_INDEX_PATTERN = 'MACHINE_LEARNING_INDEX_PATTERN',
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ export const performBulkActionSchema = t.intersection([
]),
]);

export const performBulkActionQuerySchema = t.exact(
t.partial({
dry_run: t.union([t.literal('true'), t.literal('false')]),
})
);

export type PerformBulkActionSchema = t.TypeOf<typeof performBulkActionSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import {
waitForBulkEditActionToFinish,
confirmBulkEditForm,
clickAddIndexPatternsMenuItem,
waitForElasticRulesBulkEditModal,
checkElasticRulesCannotBeModified,
checkMachineLearningRulesCannotBeModified,
waitForMixedRulesBulkEditModal,
openBulkEditAddTagsForm,
openBulkEditDeleteTagsForm,
Expand All @@ -54,14 +55,15 @@ import { hasIndexPatterns } from '../../tasks/rule_details';
import { login, visitWithoutDateRange } from '../../tasks/login';

import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation';
import { createCustomRule } from '../../tasks/api_calls/rules';
import { createCustomRule, createMachineLearningRule } from '../../tasks/api_calls/rules';
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
import {
getExistingRule,
getNewOverrideRule,
getNewRule,
getNewThresholdRule,
totalNumberOfPrebuiltRules,
getMachineLearningRule,
} from '../../objects/rule';
import { esArchiverResetKibana } from '../../tasks/es_archiver';

Expand All @@ -78,6 +80,10 @@ const customRule = {
name: RULE_NAME,
};

const expectedNumberOfCustomRulesToBeEdited = 6;
const expectedNumberOfMachineLearningRulesToBeEdited = 1;
const numberOfRulesPerPage = 5;

describe('Detection rules, bulk edit', () => {
before(() => {
cleanKibana();
Expand All @@ -96,7 +102,9 @@ describe('Detection rules, bulk edit', () => {
waitForRulesTableToBeLoaded();
});

it('should show modal windows when Elastic rules selected and edit only custom rules', () => {
it('should show warning modal windows when some of the selected rules cannot be edited', () => {
createMachineLearningRule(getMachineLearningRule(), '7');

cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN)
.pipe(($el) => $el.trigger('click'))
.should('not.exist');
Expand All @@ -107,17 +115,25 @@ describe('Detection rules, bulk edit', () => {
waitForRulesTableToBeRefreshed();

// check modal window for few selected rules
selectNumberOfRules(5);
selectNumberOfRules(numberOfRulesPerPage);
clickAddIndexPatternsMenuItem();
waitForElasticRulesBulkEditModal(5);
checkElasticRulesCannotBeModified(numberOfRulesPerPage);
cy.get(MODAL_CONFIRMATION_BTN).click();

// Select Elastic rules and custom rules, check mixed rules warning modal window, proceed with editing custom rules
// Select all rules(Elastic rules and custom)
cy.get(ELASTIC_RULES_BTN).click();
selectAllRules();
clickAddIndexPatternsMenuItem();
waitForMixedRulesBulkEditModal(totalNumberOfPrebuiltRules, 6);
cy.get(MODAL_CONFIRMATION_BTN).should('have.text', 'Edit custom rules').click();
waitForMixedRulesBulkEditModal(expectedNumberOfCustomRulesToBeEdited);

// check rules that cannot be edited for index patterns: immutable and ML
checkElasticRulesCannotBeModified(totalNumberOfPrebuiltRules);
checkMachineLearningRulesCannotBeModified(expectedNumberOfMachineLearningRulesToBeEdited);

// proceed with custom rule editing
cy.get(MODAL_CONFIRMATION_BTN)
.should('have.text', `Edit ${expectedNumberOfCustomRulesToBeEdited} Custom rules`)
.click();

typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
Expand All @@ -132,15 +148,15 @@ describe('Detection rules, bulk edit', () => {

it('should add/delete/overwrite index patterns in rules', () => {
cy.log('Adds index patterns');
// Switch to 5 rules per page, so we can edit all existing rules, not only ones on a page
// Switch to 5(numberOfRulesPerPage) rules per page, so we can edit all existing rules, not only ones on a page
// this way we will use underlying bulk edit API with query parameter, which update all rules based on query search results
changeRowsPerPageTo(5);
changeRowsPerPageTo(numberOfRulesPerPage);
selectAllRules();

openBulkEditAddIndexPatternsForm();
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });

// check if rule has been updated
changeRowsPerPageTo(20);
Expand All @@ -155,7 +171,7 @@ describe('Detection rules, bulk edit', () => {
openBulkEditDeleteIndexPatternsForm();
typeIndexPatterns([CUSTOM_INDEX_PATTERN_1]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });

// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
Expand All @@ -170,11 +186,11 @@ describe('Detection rules, bulk edit', () => {
.click();
cy.get(RULES_BULK_EDIT_INDEX_PATTERNS_WARNING).should(
'have.text',
'You’re about to overwrite index patterns for 6 selected rules, press Save to apply changes.'
`You’re about to overwrite index patterns for ${expectedNumberOfCustomRulesToBeEdited} selected rules, press Save to apply changes.`
);
typeIndexPatterns(OVERWRITE_INDEX_PATTERNS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });

// check if rule has been updated
goToTheRuleDetailsOf(RULE_NAME);
Expand All @@ -183,16 +199,16 @@ describe('Detection rules, bulk edit', () => {

it('should add/delete/overwrite tags in rules', () => {
cy.log('Add tags to all rules');
// Switch to 5 rules per page, so we can edit all existing rules, not only ones on a page
// Switch to 5(numberOfRulesPerPage) rules per page, so we can edit all existing rules, not only ones on a page
// this way we will use underlying bulk edit API with query parameter, which update all rules based on query search results
changeRowsPerPageTo(5);
changeRowsPerPageTo(numberOfRulesPerPage);
selectAllRules();

// open add tags form and add 2 new tags
openBulkEditAddTagsForm();
typeTags(TAGS);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });

// check if all rules have been updated with new tags
changeRowsPerPageTo(20);
Expand All @@ -208,7 +224,7 @@ describe('Detection rules, bulk edit', () => {
openBulkEditDeleteTagsForm();
typeTags([TAGS[0]]);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });

testAllTagsBadges(TAGS.slice(1));
cy.get(RULES_TAGS_FILTER_BTN).contains(/Tags1/);
Expand All @@ -220,19 +236,19 @@ describe('Detection rules, bulk edit', () => {
.click();
cy.get(RULES_BULK_EDIT_TAGS_WARNING).should(
'have.text',
'You’re about to overwrite tags for 6 selected rules, press Save to apply changes.'
`You’re about to overwrite tags for ${expectedNumberOfCustomRulesToBeEdited} selected rules, press Save to apply changes.`
);
typeTags(['overwrite-tag']);
confirmBulkEditForm();
waitForBulkEditActionToFinish({ rulesCount: 6 });
waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited });

testAllTagsBadges(['overwrite-tag']);
});

it('should not lose rules selection after edit action', () => {
const rulesCount = 4;
// Switch to 5 rules per page, to have few pages in pagination(ideal way to test auto refresh and selection of few items)
changeRowsPerPageTo(5);
changeRowsPerPageTo(numberOfRulesPerPage);
selectNumberOfRules(rulesCount);

// open add tags form and add 2 new tags
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/security_solution/cypress/objects/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export interface ThreatIndicatorRule extends CustomRule {

export interface MachineLearningRule {
machineLearningJobs: string[];
anomalyScoreThreshold: string;
anomalyScoreThreshold: number;
name: string;
description: string;
severity: string;
Expand All @@ -102,6 +102,7 @@ export interface MachineLearningRule {
note: string;
runsEvery: Interval;
lookBack: Interval;
interval?: string;
}

export const getIndexPatterns = (): string[] => [
Expand Down Expand Up @@ -326,7 +327,7 @@ export const getMachineLearningRule = (): MachineLearningRule => ({
'v3_linux_anomalous_process_all_hosts',
'v3_linux_anomalous_network_activity',
],
anomalyScoreThreshold: '20',
anomalyScoreThreshold: 20,
name: 'New ML Rule Test',
description: 'The new ML rule description.',
severity: 'Critical',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,28 @@
* 2.0.
*/

import type { CustomRule, ThreatIndicatorRule } from '../../objects/rule';
import type { CustomRule, ThreatIndicatorRule, MachineLearningRule } from '../../objects/rule';

export const createMachineLearningRule = (rule: MachineLearningRule, ruleId = 'ml_rule_testing') =>
cy.request({
method: 'POST',
url: 'api/detection_engine/rules',
body: {
rule_id: ruleId,
risk_score: parseInt(rule.riskScore, 10),
description: rule.description,
interval: rule.interval,
name: rule.name,
severity: rule.severity.toLocaleLowerCase(),
type: 'machine_learning',
from: 'now-50000h',
enabled: false,
machine_learning_job_id: rule.machineLearningJobs,
anomaly_threshold: rule.anomalyScoreThreshold,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
failOnStatusCode: false,
});

export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') =>
cy.request({
Expand Down
29 changes: 13 additions & 16 deletions x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,27 +78,24 @@ export const waitForBulkEditActionToFinish = ({ rulesCount }: { rulesCount: numb
};

export const waitForElasticRulesBulkEditModal = (rulesCount: number) => {
cy.get(MODAL_CONFIRMATION_TITLE).should(
'have.text',
`${rulesCount} Elastic rules cannot be edited`
);
cy.get(MODAL_CONFIRMATION_BODY).should(
'have.text',
'Elastic rules are not modifiable. The update action will only be applied to Custom rules.'
cy.get(MODAL_CONFIRMATION_TITLE).should('have.text', `${rulesCount} rules cannot be edited`);
};

export const checkElasticRulesCannotBeModified = (rulesCount: number) => {
cy.get(MODAL_CONFIRMATION_BODY).contains(
`${rulesCount} prebuilt Elastic rules (editing prebuilt rules is not supported)`
);
};

export const waitForMixedRulesBulkEditModal = (
elasticRulesCount: number,
customRulesCount: number
) => {
cy.get(MODAL_CONFIRMATION_TITLE).should(
'have.text',
`${elasticRulesCount} Elastic rules cannot be edited`
export const checkMachineLearningRulesCannotBeModified = (rulesCount: number) => {
cy.get(MODAL_CONFIRMATION_BODY).contains(
`${rulesCount} custom Machine Learning rule (these rules don't have index patterns)`
);
};

cy.get(MODAL_CONFIRMATION_BODY).should(
export const waitForMixedRulesBulkEditModal = (customRulesCount: number) => {
cy.get(MODAL_CONFIRMATION_TITLE).should(
'have.text',
`The update action will only be applied to ${customRulesCount} Custom rules you've selected.`
`The action will only be applied to ${customRulesCount} Custom rules you've selected`
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export const pureFetchRuleById = async ({
* @param ids string[] rule ids to select rules to perform bulk action with
* @param edit BulkEditActionPayload edit action payload
* @param action bulk action to perform
* @param isDryRun enables dry run mode for bulk actions
*
* @throws An error if response is not OK
*/
Expand All @@ -215,6 +216,7 @@ export const performBulkAction = async <Action extends BulkAction>({
query,
edit,
ids,
isDryRun,
}: BulkActionProps<Action>): Promise<BulkActionResponseMap<Action>> =>
KibanaServices.get().http.fetch<BulkActionResponseMap<Action>>(
DETECTION_ENGINE_RULES_BULK_ACTION,
Expand All @@ -226,6 +228,9 @@ export const performBulkAction = async <Action extends BulkAction>({
...(ids ? { ids } : {}),
...(query !== undefined ? { query } : {}),
}),
query: {
...(isDryRun ? { dry_run: isDryRun } : {}),
},
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import * as t from 'io-ts';

import { listArray } from '@kbn/securitysolution-io-ts-list-types';
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import {
risk_score_mapping,
threat_query,
Expand Down Expand Up @@ -52,6 +53,8 @@ import type {
UpdateRulesSchema,
} from '../../../../../common/detection_engine/schemas/request';

import type { BulkActionsDryRunErrCode } from '../../../../../common/constants';

/**
* Params is an "record", since it is a type of RuleActionParams which is action templates.
* @see x-pack/plugins/alerting/common/rule.ts
Expand Down Expand Up @@ -223,6 +226,7 @@ export interface FilterOptions {
showCustomRules: boolean;
showElasticRules: boolean;
tags: string[];
excludeRuleTypes?: Type[];
}

export interface FetchRulesResponse {
Expand All @@ -242,6 +246,7 @@ export interface BulkActionProps<Action extends BulkAction> {
query?: string;
ids?: string[];
edit?: BulkActionEditPayload[];
isDryRun?: boolean;
}

export interface BulkActionSummary {
Expand All @@ -259,6 +264,7 @@ export interface BulkActionResult {
export interface BulkActionAggregatedError {
message: string;
status_code: number;
err_code?: BulkActionsDryRunErrCode;
rules: Array<{ id: string; name?: string }>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,13 @@ describe('convertRulesFilterToKQL', () => {
`alert.attributes.params.immutable: true AND alert.attributes.tags:("tag1" AND "tag2") AND (alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")`
);
});

it('handles presence of "excludeRuleTypes" properly', () => {
const kql = convertRulesFilterToKQL({
...filterOptions,
excludeRuleTypes: ['machine_learning', 'saved_query'],
});

expect(kql).toBe('NOT alert.attributes.params.type: ("machine_learning" OR "saved_query")');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const convertRulesFilterToKQL = ({
showElasticRules,
filter,
tags,
excludeRuleTypes = [],
}: FilterOptions): string => {
const filters: string[] = [];

Expand All @@ -56,5 +57,13 @@ export const convertRulesFilterToKQL = ({
filters.push(`(${searchQuery})`);
}

if (excludeRuleTypes.length) {
filters.push(
`NOT alert.attributes.params.type: (${excludeRuleTypes
.map((ruleType) => `"${escapeKuery(ruleType)}"`)
.join(' OR ')})`
);
}

return filters.join(' AND ');
};
Loading