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 confirmation modal window for bulk exporting action #136418

Merged
merged 94 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 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
4a242a7
export initial
vitaliidm Jul 14, 2022
d1fe340
fix tests
vitaliidm Jul 18, 2022
3432fca
updates
vitaliidm Jul 18, 2022
008fa51
fixes
vitaliidm Jul 18, 2022
55942cd
Merge branch 'main' into fix/bulk-export-modal
vitaliidm Jul 18, 2022
42f501f
remove
vitaliidm Jul 18, 2022
321f52d
fix 1
vitaliidm Jul 18, 2022
296810b
rename
vitaliidm Jul 18, 2022
0b6fc00
update
vitaliidm Jul 18, 2022
c6f52f1
fix 2
vitaliidm Jul 18, 2022
cc23e97
Rename bulk_action_rule_errors_list.tsx_test to bulk_action_rule_erro…
vitaliidm Jul 18, 2022
12a6eb5
Merge branch 'main' into fix/bulk-export-modal
vitaliidm Jul 18, 2022
b896e04
Merge branch 'main' into fix/bulk-export-modal
vitaliidm Jul 18, 2022
5245f9e
small refactoring
vitaliidm Jul 19, 2022
62a98e2
Merge branch 'fix/bulk-export-modal' of https://github.com/vitaliidm/…
vitaliidm Jul 19, 2022
ccd1bcb
another refactoring
vitaliidm Jul 19, 2022
39de6fd
add cypress tests
vitaliidm Jul 19, 2022
0a01de6
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Jul 19, 2022
4b9877d
comment wording
vitaliidm Jul 19, 2022
21b353d
Merge branch 'fix/bulk-export-modal' of https://github.com/vitaliidm/…
vitaliidm Jul 19, 2022
f8fe3ee
change toast messages
vitaliidm Jul 20, 2022
98904a6
rename file
vitaliidm Jul 20, 2022
2821954
Merge branch 'main' into fix/bulk-export-modal
vitaliidm Jul 20, 2022
d0d31b1
final review
vitaliidm Jul 20, 2022
e7cb45c
Merge branch 'main' into fix/bulk-export-modal
kibanamachine Jul 22, 2022
8a3008e
CR feeback
vitaliidm Jul 22, 2022
9ec26a2
Merge branch 'main' into fix/bulk-export-modal
vitaliidm Jul 25, 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
CUSTOM_RULES_BTN,
MODAL_CONFIRMATION_BTN,
SELECT_ALL_RULES_ON_PAGE_CHECKBOX,
LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN,
RULES_TAGS_FILTER_BTN,
RULE_CHECKBOX,
RULES_TAGS_POPOVER_BTN,
Expand All @@ -29,11 +28,12 @@ import {
waitForRulesTableToBeLoaded,
selectAllRules,
goToTheRuleDetailsOf,
waitForRulesTableToBeRefreshed,
selectNumberOfRules,
testAllTagsBadges,
testTagsBadge,
testMultipleSelectedRulesLabel,
loadPrebuiltDetectionRulesFromHeaderBtn,
switchToElasticRules,
} from '../../tasks/alerts_detection_rules';

import {
Expand Down Expand Up @@ -105,14 +105,11 @@ describe('Detection rules, bulk edit', () => {
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');
loadPrebuiltDetectionRulesFromHeaderBtn();

// select few Elastic rules, check if we can't proceed further, as ELastic rules are not editable
// filter rules, only Elastic rule to show
cy.get(ELASTIC_RULES_BTN).click();
waitForRulesTableToBeRefreshed();
switchToElasticRules();

// check modal window for few selected rules
selectNumberOfRules(numberOfRulesPerPage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@
* 2.0.
*/

import { expectedExportedRule, getNewRule } from '../../objects/rule';
import { expectedExportedRule, getNewRule, totalNumberOfPrebuiltRules } from '../../objects/rule';

import { TOASTER_BODY } from '../../screens/alerts_detection_rules';
import {
TOASTER_BODY,
MODAL_CONFIRMATION_BODY,
MODAL_CONFIRMATION_BTN,
} from '../../screens/alerts_detection_rules';

import { exportFirstRule } from '../../tasks/alerts_detection_rules';
import {
exportFirstRule,
loadPrebuiltDetectionRulesFromHeaderBtn,
switchToElasticRules,
selectNumberOfRules,
bulkExportRules,
selectAllRules,
} from '../../tasks/alerts_detection_rules';
import { createCustomRule } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common';
import { login, visitWithoutDateRange } from '../../tasks/login';

import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
Expand All @@ -23,6 +34,7 @@ describe('Export rules', () => {
});

beforeEach(() => {
deleteAlertsAndRules();
// Rules get exported via _bulk_action endpoint
cy.intercept('POST', '/api/detection_engine/rules/_bulk_action').as('bulk_action');
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
Expand All @@ -33,10 +45,45 @@ describe('Export rules', () => {
exportFirstRule();
cy.wait('@bulk_action').then(({ response }) => {
cy.wrap(response?.body).should('eql', expectedExportedRule(this.ruleResponse));
cy.get(TOASTER_BODY).should(
'have.text',
'Successfully exported 1 of 1 rule. Prebuilt rules were excluded from the resulting file.'
);
cy.get(TOASTER_BODY).should('have.text', 'Successfully exported 1 of 1 rule.');
});
});

it('shows a modal saying that no rules can be exported if all the selected rules are prebuilt', function () {
const expectedElasticRulesCount = 7;

loadPrebuiltDetectionRulesFromHeaderBtn();

switchToElasticRules();
selectNumberOfRules(expectedElasticRulesCount);
bulkExportRules();

cy.get(MODAL_CONFIRMATION_BODY).contains(
`${expectedElasticRulesCount} prebuilt Elastic rules (exporting prebuilt rules is not supported)`
);
});

it('exports only custom rules', function () {
const expectedNumberCustomRulesToBeExported = 1;
const totalNumberOfRules = expectedNumberCustomRulesToBeExported + totalNumberOfPrebuiltRules;

loadPrebuiltDetectionRulesFromHeaderBtn();

selectAllRules();
bulkExportRules();

cy.get(MODAL_CONFIRMATION_BODY).contains(
`${totalNumberOfPrebuiltRules} prebuilt Elastic rules (exporting prebuilt rules is not supported)`
);

// proceed with exporting only custom rules
cy.get(MODAL_CONFIRMATION_BTN)
.should('have.text', `Export ${expectedNumberCustomRulesToBeExported} Custom rule`)
.click();

cy.get(TOASTER_BODY).should(
'contain',
`Successfully exported ${expectedNumberCustomRulesToBeExported} of ${totalNumberOfRules} rules. Prebuilt rules were excluded from the resulting file.`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const ELASTIC_RULES_BTN = '[data-test-subj="showElasticRulesFilterButton"

export const EXPORT_ACTION_BTN = '[data-test-subj="exportRuleAction"]';

export const BULK_EXPORT_ACTION_BTN = '[data-test-subj="exportRuleBulk"]';

export const FIRST_RULE = 0;

export const FOURTH_RULE = 3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DELETE_RULE_ACTION_BTN,
DELETE_RULE_BULK_BTN,
LOAD_PREBUILT_RULES_BTN,
LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN,
RULES_TABLE_INITIAL_LOADING_INDICATOR,
RULES_TABLE_REFRESH_INDICATOR,
RULES_TABLE_AUTOREFRESH_INDICATOR,
Expand Down Expand Up @@ -50,6 +51,8 @@ import {
SELECTED_RULES_NUMBER_LABEL,
REFRESH_SETTINGS_POPOVER,
REFRESH_SETTINGS_SWITCH,
ELASTIC_RULES_BTN,
BULK_EXPORT_ACTION_BTN,
} from '../screens/alerts_detection_rules';
import { ALL_ACTIONS } from '../screens/rule_details';
import { LOADING_INDICATOR } from '../screens/security_header';
Expand Down Expand Up @@ -168,6 +171,12 @@ export const loadPrebuiltDetectionRules = () => {
.should('be.disabled');
};

export const loadPrebuiltDetectionRulesFromHeaderBtn = () => {
cy.get(LOAD_PREBUILT_RULES_ON_PAGE_HEADER_BTN)
.pipe(($el) => $el.trigger('click'))
.should('not.exist');
};

export const openIntegrationsPopover = () => {
cy.get(INTEGRATIONS_POPOVER).click();
};
Expand Down Expand Up @@ -328,3 +337,13 @@ export const mockGlobalClock = () => {

cy.clock(Date.now(), ['setInterval', 'clearInterval', 'Date']);
};

export const switchToElasticRules = () => {
cy.get(ELASTIC_RULES_BTN).click();
waitForRulesTableToBeRefreshed();
};

export const bulkExportRules = () => {
cy.get(BULK_ACTIONS_BTN).click();
cy.get(BULK_EXPORT_ACTION_BTN).click();
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { Rule } from '../../../containers/detection_engine/rules';
import {
executeRulesBulkAction,
goToRuleEditPage,
bulkExportRules,
} from '../../../pages/detection_engine/rules/all/actions';
import * as i18nActions from '../../../pages/detection_engine/rules/translations';
import * as i18n from './translations';
Expand Down Expand Up @@ -108,7 +109,7 @@ const RuleActionsOverflowComponent = ({
onClick={async () => {
startTransaction({ name: SINGLE_RULE_ACTIONS.EXPORT });
closePopover();
await executeRulesBulkAction({
await bulkExportRules({
action: BulkAction.export,
onSuccess: noop,
search: { ids: [rule.id] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import type { UseAppToasts } from '../../../../../common/hooks/use_app_toasts';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../../common/lib/telemetry';
import { downloadBlob } from '../../../../../common/utils/download_blob';
import type {
BulkActionResponse,
BulkActionSummary,
BulkActionResponse,
} from '../../../../containers/detection_engine/rules';
import { performBulkAction } from '../../../../containers/detection_engine/rules';
import * as i18n from '../translations';
Expand All @@ -34,19 +34,39 @@ export const goToRuleEditPage = (
});
};

interface ExecuteRulesBulkActionArgs {
type OnActionSuccessCallback = (
toasts: UseAppToasts,
action: BulkAction,
summary: BulkActionSummary
) => void;

type OnActionErrorCallback = (toasts: UseAppToasts, action: BulkAction, error: HTTPError) => void;

interface BaseRulesBulkActionArgs {
visibleRuleIds?: string[];
action: BulkAction;
toasts: UseAppToasts;
search: { query: string } | { ids: string[] };
payload?: { edit?: BulkActionEditPayload[] };
onSuccess?: (toasts: UseAppToasts, action: BulkAction, summary: BulkActionSummary) => void;
onError?: (toasts: UseAppToasts, action: BulkAction, error: HTTPError) => void;
onError?: OnActionErrorCallback;
onFinish?: () => void;
onSuccess?: OnActionSuccessCallback;
setLoadingRules?: RulesTableActions['setLoadingRules'];
}

export const executeRulesBulkAction = async ({
interface RulesBulkActionArgs extends BaseRulesBulkActionArgs {
action: Exclude<BulkAction, BulkAction.export>;
}
interface ExportRulesBulkActionArgs extends BaseRulesBulkActionArgs {
action: BulkAction.export;
}

// export bulk actions API returns blob, the rest of actions returns BulkActionResponse object
// hence method overloading to make type safe calls
export async function executeRulesBulkAction(args: ExportRulesBulkActionArgs): Promise<Blob | null>;
export async function executeRulesBulkAction(
args: RulesBulkActionArgs
): Promise<BulkActionResponse | null>;
export async function executeRulesBulkAction({
visibleRuleIds = [],
action,
setLoadingRules,
Expand All @@ -56,28 +76,66 @@ export const executeRulesBulkAction = async ({
onSuccess = defaultSuccessHandler,
onError = defaultErrorHandler,
onFinish,
}: ExecuteRulesBulkActionArgs) => {
}: RulesBulkActionArgs | ExportRulesBulkActionArgs) {
let response: Blob | BulkActionResponse | null = null;
try {
setLoadingRules?.({ ids: visibleRuleIds, action });

if (action === BulkAction.export) {
const response = await performBulkAction({ ...search, action });
downloadBlob(response, `${i18n.EXPORT_FILENAME}.ndjson`);
onSuccess(toasts, action, await getExportedRulesCounts(response));
// on successToast for export handles separately outside of action execution method
response = await performBulkAction({ ...search, action });
} else {
const response = await performBulkAction({ ...search, action, edit: payload?.edit });
response = await performBulkAction({ ...search, action, edit: payload?.edit });
sendTelemetry(action, response);
onSuccess(toasts, action, response.attributes.summary);

return response;
}
} catch (error) {
onError(toasts, action, error);
} finally {
setLoadingRules?.({ ids: [], action: null });
onFinish?.();
}
};
return response;
}

/**
* downloads exported rules, received from export action
* @param params.response - Blob results with exported rules
* @param params.toasts - {@link UseAppToasts} toasts service
* @param params.onSuccess - {@link OnActionSuccessCallback} optional toast to display when action successful
* @param params.onError - {@link OnActionErrorCallback} optional toast to display when action failed
*/
export async function downloadExportedRules({
response,
toasts,
onSuccess = defaultSuccessHandler,
onError = defaultErrorHandler,
}: {
response: Blob;
toasts: UseAppToasts;
onSuccess?: OnActionSuccessCallback;
onError?: OnActionErrorCallback;
}) {
try {
downloadBlob(response, `${i18n.EXPORT_FILENAME}.ndjson`);
onSuccess(toasts, BulkAction.export, await getExportedRulesCounts(response));
} catch (error) {
onError(toasts, BulkAction.export, error);
}
}

/**
* executes bulk export action and downloads exported rules
* @param params - {@link ExportRulesBulkActionArgs}
*/
export async function bulkExportRules(params: ExportRulesBulkActionArgs) {
const response = await executeRulesBulkAction(params);

// if response null, likely network error happened and export rules haven't been received
if (response) {
await downloadExportedRules({ response, toasts: params.toasts, onSuccess: params.onSuccess });
}
banderror marked this conversation as resolved.
Show resolved Hide resolved
}

function defaultErrorHandler(toasts: UseAppToasts, action: BulkAction, error: HTTPError) {
// if response doesn't have number of failed rules, it means the whole bulk action failed
Expand Down Expand Up @@ -130,6 +188,18 @@ function defaultErrorHandler(toasts: UseAppToasts, action: BulkAction, error: HT
toasts.addError(error, { title, toastMessage });
}

const getExportSuccessToastMessage = (succeeded: number, total: number) => {
const message = [i18n.RULES_BULK_EXPORT_SUCCESS_DESCRIPTION(succeeded, total)];

// if not all rules are successfully exported it means there included prebuilt rules
// display message to users that prebuilt rules were excluded
if (total > succeeded) {
message.push(i18n.RULES_BULK_EXPORT_PREBUILT_RULES_EXCLUDED_DESCRIPTION);
}

return message.join(' ');
};

async function defaultSuccessHandler(
toasts: UseAppToasts,
action: BulkAction,
Expand All @@ -141,7 +211,7 @@ async function defaultSuccessHandler(
switch (action) {
case BulkAction.export:
title = i18n.RULES_BULK_EXPORT_SUCCESS;
text = i18n.RULES_BULK_EXPORT_SUCCESS_DESCRIPTION(summary.succeeded, summary.total);
text = getExportSuccessToastMessage(summary.succeeded, summary.total);
vitaliidm marked this conversation as resolved.
Show resolved Hide resolved
break;
case BulkAction.duplicate:
title = i18n.RULES_BULK_DUPLICATE_SUCCESS;
Expand Down
Loading