From 5ce47b276ae657d07b5774d11b7cad4a50c44be3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Mon, 11 Nov 2024 09:29:21 -0300 Subject: [PATCH] [Security Solution] Rule Updates in bulk with conflicts (#196776) Resolves: https://github.com/elastic/kibana/issues/180589 ## Summary - Handles bulk updating of rules with conflicts in the Rule Upgrades table. See detailed requirements implemented in ticket linked above. - Changes default `pick_version` of both the `/upgrade/_perform` endpoint, and of the request payloads for that endpoint from the frontend, from `TARGET` to `MERGED`, when the `isPrebuiltRulesCustomizationEnabled` is `true`. - **Also:** handles issue in `/upgrade/_perform` endpoint with the `index` and `data_view_id` fields. See file: `x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/diffable_rule_fields_mappings.ts`. **See demo video:** https://www.loom.com/share/90d94d2a8f16442b9a43a425eeab6697 **New copy in warning modal** image **Newly added tooltips:** ![image](https://github.com/user-attachments/assets/7ada117e-57a7-4699-ad08-312c734586d9) ![image](https://github.com/user-attachments/assets/c8ed80ac-c1c3-48f1-8f8e-2433415a6772) ![image](https://github.com/user-attachments/assets/d77ed6f0-5d65-4933-9012-f6cd153a9350) ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels) - [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com> Co-authored-by: Dmitrii --- .github/CODEOWNERS | 1 - .../machine_learning}/affected_job_ids.ts | 0 .../rule_management/api/api.ts | 16 +- .../use_perform_all_rules_upgrade_mutation.ts | 49 ------ ...perform_specific_rules_upgrade_mutation.ts | 4 +- .../use_perform_rule_upgrade.ts | 23 +-- .../modals/ml_job_upgrade_modal/index.tsx | 0 .../ml_job_upgrade_modal/translations.tsx | 2 +- .../modals/upgrade_conflicts_modal/index.tsx | 36 +++++ .../upgrade_conflicts_modal/translations.tsx | 37 +++++ .../translations.ts | 27 ++++ .../upgrade_prebuilt_rules_table_buttons.tsx | 129 ++++++++++++--- .../upgrade_prebuilt_rules_table_context.tsx | 128 ++++++++++----- .../use_upgrade_modals.tsx | 45 ++++++ ...e_upgrade_prebuilt_rules_table_columns.tsx | 22 ++- .../ml_job_compatibility_callout/index.tsx | 2 +- .../detection_engine/rules/translations.ts | 7 + .../create_upgradeable_rules_payload.ts | 7 +- .../perform_rule_upgrade_route.ts | 8 +- .../prebuilt_rules/api/register_routes.ts | 9 +- .../security_solution/server/routes/index.ts | 2 +- ...low_with_prebuilt_rule_customization.cy.ts | 153 +++++++++++++++++- .../cypress/screens/rule_updates.ts | 2 + .../cypress/tasks/prebuilt_rules.ts | 9 ++ 24 files changed, 555 insertions(+), 163 deletions(-) rename x-pack/plugins/security_solution/{public/detections/components/callouts/ml_job_compatibility_callout => common/machine_learning}/affected_job_ids.ts (100%) delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts rename x-pack/plugins/security_solution/public/{detections/components => detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table}/modals/ml_job_upgrade_modal/index.tsx (100%) rename x-pack/plugins/security_solution/public/{detections/components => detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table}/modals/ml_job_upgrade_modal/translations.tsx (95%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_modals.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 73a670d14534..abc6749d52ea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1843,7 +1843,6 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/security_solution/public/detection_engine/rule_management_ui @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detection_engine/rule_monitoring @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detections/components/callouts @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detections/components/rules @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detections/components/rules/rule_preview @elastic/security-detection-engine /x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules @elastic/security-detection-rule-management diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/affected_job_ids.ts b/x-pack/plugins/security_solution/common/machine_learning/affected_job_ids.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/affected_job_ids.ts rename to x-pack/plugins/security_solution/common/machine_learning/affected_job_ids.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 1e2ee1be7a47..aea4b6672659 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -16,6 +16,7 @@ import type { ActionResult } from '@kbn/actions-plugin/server'; import { convertRulesFilterToKQL } from '../../../../common/detection_engine/rule_management/rule_filtering'; import type { UpgradeSpecificRulesRequest, + PickVersionValues, PerformRuleUpgradeResponseBody, InstallSpecificRulesRequest, PerformRuleInstallationResponseBody, @@ -678,18 +679,9 @@ export const performInstallSpecificRules = async ( }), }); -export const performUpgradeAllRules = async (): Promise => - KibanaServices.get().http.fetch(PERFORM_RULE_UPGRADE_URL, { - method: 'POST', - version: '1', - body: JSON.stringify({ - mode: 'ALL_RULES', - pick_version: 'TARGET', - }), - }); - export const performUpgradeSpecificRules = async ( - rules: UpgradeSpecificRulesRequest['rules'] + rules: UpgradeSpecificRulesRequest['rules'], + pickVersion: PickVersionValues ): Promise => KibanaServices.get().http.fetch(PERFORM_RULE_UPGRADE_URL, { method: 'POST', @@ -697,7 +689,7 @@ export const performUpgradeSpecificRules = async ( body: JSON.stringify({ mode: 'SPECIFIC_RULES', rules, - pick_version: 'TARGET', // Setting fixed 'TARGET' temporarily for Milestone 2 + pick_version: pickVersion, }), }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts deleted file mode 100644 index 7e5385bb0c75..000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation.ts +++ /dev/null @@ -1,49 +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 { UseMutationOptions } from '@tanstack/react-query'; -import { useMutation } from '@tanstack/react-query'; -import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; -import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; -import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; -import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_prebuilt_rules_upgrade_review_query'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; -import { performUpgradeAllRules } from '../../api'; -import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; - -export const PERFORM_ALL_RULES_UPGRADE_KEY = ['POST', 'ALL_RULES', PERFORM_RULE_UPGRADE_URL]; - -export const usePerformAllRulesUpgradeMutation = ( - options?: UseMutationOptions -) => { - const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); - const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); - const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); - const invalidateFetchPrebuiltRulesUpgradeReview = - useInvalidateFetchPrebuiltRulesUpgradeReviewQuery(); - const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); - const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); - - return useMutation(() => performUpgradeAllRules(), { - ...options, - mutationKey: PERFORM_ALL_RULES_UPGRADE_KEY, - onSettled: (...args) => { - invalidateFindRulesQuery(); - invalidateFetchRulesSnoozeSettings(); - invalidateFetchRuleManagementFilters(); - - invalidateFetchPrebuiltRulesUpgradeReview(); - invalidateRuleStatus(); - invalidateFetchCoverageOverviewQuery(); - - if (options?.onSettled) { - options.onSettled(...args); - } - }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts index c10b92ea914f..08338ab9a932 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts @@ -8,6 +8,7 @@ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; import type { PerformRuleUpgradeResponseBody, + PickVersionValues, UpgradeSpecificRulesRequest, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; @@ -26,6 +27,7 @@ export const PERFORM_SPECIFIC_RULES_UPGRADE_KEY = [ ]; export const usePerformSpecificRulesUpgradeMutation = ( + pickVersion: PickVersionValues, options?: UseMutationOptions< PerformRuleUpgradeResponseBody, Error, @@ -43,7 +45,7 @@ export const usePerformSpecificRulesUpgradeMutation = ( return useMutation( (rulesToUpgrade: UpgradeSpecificRulesRequest['rules']) => { - return performUpgradeSpecificRules(rulesToUpgrade); + return performUpgradeSpecificRules(rulesToUpgrade, pickVersion); }, { ...options, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts index aa9e38217a19..f82812f7ac9d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts @@ -4,29 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { PickVersionValues } from '../../../../../common/api/detection_engine'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { usePerformAllRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_all_rules_upgrade_mutation'; import { usePerformSpecificRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation'; import * as i18n from './translations'; -export const usePerformUpgradeAllRules = () => { - const { addError, addSuccess } = useAppToasts(); - - return usePerformAllRulesUpgradeMutation({ - onError: (err) => { - addError(err, { title: i18n.RULE_UPGRADE_FAILED }); - }, - onSuccess: (result) => { - addSuccess(getSuccessToastMessage(result)); - }, - }); -}; - -export const usePerformUpgradeSpecificRules = () => { +export const usePerformUpgradeSpecificRules = ({ + pickVersion, +}: { + pickVersion: PickVersionValues; +}) => { const { addError, addSuccess } = useAppToasts(); - return usePerformSpecificRulesUpgradeMutation({ + return usePerformSpecificRulesUpgradeMutation(pickVersion, { onError: (err) => { addError(err, { title: i18n.RULE_UPGRADE_FAILED }); }, diff --git a/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/ml_job_upgrade_modal/index.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/ml_job_upgrade_modal/index.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/ml_job_upgrade_modal/translations.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/ml_job_upgrade_modal/translations.tsx index 8163eca279cf..caf94e5c1a26 100644 --- a/x-pack/plugins/security_solution/public/detections/components/modals/ml_job_upgrade_modal/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/ml_job_upgrade_modal/translations.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { MlJobCompatibilityLink } from '../../../../common/components/links_to_docs'; +import { MlJobCompatibilityLink } from '../../../../../../../common/components/links_to_docs'; export const ML_JOB_UPGRADE_MODAL_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.mlJobUpgradeModal.messageTitle', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/index.tsx new file mode 100644 index 000000000000..0c664398c51f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/index.tsx @@ -0,0 +1,36 @@ +/* + * 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 { EuiConfirmModal, EuiText } from '@elastic/eui'; +import React, { memo } from 'react'; +import * as i18n from './translations'; + +export interface UpgradeConflictsModalProps { + onCancel: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + onConfirm?: (event: React.MouseEvent) => void; +} + +const UpgradeConflictsModalComponent = ({ onCancel, onConfirm }: UpgradeConflictsModalProps) => { + return ( + + {i18n.UPGRADE_CONFLICTS_MODAL_BODY} + + ); +}; + +export const UpgradeConflictsModal = memo(UpgradeConflictsModalComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/translations.tsx new file mode 100644 index 000000000000..bc5738b879cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/modals/upgrade_conflicts_modal/translations.tsx @@ -0,0 +1,37 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const UPGRADE_CONFLICTS_MODAL_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.messageTitle', + { + defaultMessage: 'Update rules without conflicts?', + } +); + +export const UPGRADE_CONFLICTS_MODAL_CANCEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.cancelTitle', + { + defaultMessage: 'Cancel', + } +); + +export const UPGRADE_CONFLICTS_MODAL_CONFIRM = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.confirmTitle', + { + defaultMessage: 'Update rules without conflicts', + } +); + +export const UPGRADE_CONFLICTS_MODAL_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.affectedJobsTitle', + { + defaultMessage: + "Some of the selected rules have conflicts and, for that reason, won't be updated. Resolve the conflicts to properly update the rules.", + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.ts index 026c35f664bb..5db5d0eee748 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.ts @@ -24,6 +24,27 @@ export const UPDATE_SELECTED_RULES = (numberOfSelectedRules: number) => { ); }; +export const BULK_UPDATE_BUTTON_TOOLTIP_NO_PERMISSIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.noPermissions', + { + defaultMessage: "You don't have permissions to update rules", + } +); + +export const BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.allRules.conflicts', + { + defaultMessage: 'All rules have conflicts. Update them individually.', + } +); + +export const BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.selectedRules.conflicts', + { + defaultMessage: 'All selected rules have conflicts. Update them individually.', + } +); + export const SEARCH_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.searchBarPlaceholder', { @@ -37,6 +58,12 @@ export const UPDATE_BUTTON_LABEL = i18n.translate( defaultMessage: 'Update', } ); +export const UPDATE_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.updateError', + { + defaultMessage: 'Update error', + } +); export const UPDATE_FLYOUT_PER_FIELD_TOOLTIP_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.perFieldTooltip', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx index 1d0e6adeabce..8a1d20114965 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import React, { useCallback } from 'react'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; import { useUserData } from '../../../../../detections/components/user_info'; @@ -20,8 +20,15 @@ export const UpgradePrebuiltRulesTableButtons = ({ selectedRules, }: UpgradePrebuiltRulesTableButtonsProps) => { const { - state: { hasRulesToUpgrade, loadingRules, isRefetching, isUpgradingSecurityPackages }, - actions: { upgradeAllRules, upgradeRules }, + state: { + ruleUpgradeInfos, + hasRulesToUpgrade, + loadingRules, + isRefetching, + isUpgradingSecurityPackages, + isPrebuiltRulesCustomizationEnabled, + }, + actions: { upgradeRules }, } = useUpgradePrebuiltRulesTableContext(); const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData(); const canUserEditRules = canUserCRUD && !isUserDataLoading; @@ -31,39 +38,115 @@ export const UpgradePrebuiltRulesTableButtons = ({ const isRuleUpgrading = loadingRules.length > 0; const isRequestInProgress = isRuleUpgrading || isRefetching || isUpgradingSecurityPackages; + const doAllSelectedRulesHaveConflicts = + isPrebuiltRulesCustomizationEnabled && isAllRuleHaveConflicts(selectedRules); + const doAllRulesHaveConflicts = + isPrebuiltRulesCustomizationEnabled && isAllRuleHaveConflicts(ruleUpgradeInfos); + + const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({ + canUserEditRules, + doAllSelectedRulesHaveConflicts, + doAllRulesHaveConflicts, + isPrebuiltRulesCustomizationEnabled, + }); + const upgradeSelectedRules = useCallback( () => upgradeRules(selectedRules.map((rule) => rule.rule_id)), [selectedRules, upgradeRules] ); + const upgradeAllRules = useCallback( + // Upgrade all rules, ignoring filter and selection + () => upgradeRules(ruleUpgradeInfos.map((rule) => rule.rule_id)), + [ruleUpgradeInfos, upgradeRules] + ); + return ( {shouldDisplayUpgradeSelectedRulesButton ? ( - - <> - {i18n.UPDATE_SELECTED_RULES(numberOfSelectedRules)} - {isRuleUpgrading ? : undefined} - - + + + <> + {i18n.UPDATE_SELECTED_RULES(numberOfSelectedRules)} + {isRuleUpgrading ? : undefined} + + + ) : null} - - {i18n.UPDATE_ALL} - {isRuleUpgrading ? : undefined} - + + + {i18n.UPDATE_ALL} + {isRuleUpgrading ? : undefined} + + ); }; + +const useBulkUpdateButtonsTooltipContent = ({ + canUserEditRules, + doAllSelectedRulesHaveConflicts, + doAllRulesHaveConflicts, + isPrebuiltRulesCustomizationEnabled, +}: { + canUserEditRules: boolean | null; + doAllSelectedRulesHaveConflicts: boolean; + doAllRulesHaveConflicts: boolean; + isPrebuiltRulesCustomizationEnabled: boolean; +}) => { + if (!canUserEditRules) { + return { + selectedRulesButtonTooltip: i18n.BULK_UPDATE_BUTTON_TOOLTIP_NO_PERMISSIONS, + allRulesButtonTooltip: i18n.BULK_UPDATE_BUTTON_TOOLTIP_NO_PERMISSIONS, + }; + } + + if (!isPrebuiltRulesCustomizationEnabled) { + return { + selectedRulesButtonTooltip: undefined, + allRulesButtonTooltip: undefined, + }; + } + + if (doAllRulesHaveConflicts) { + return { + selectedRulesButtonTooltip: i18n.BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS, + allRulesButtonTooltip: i18n.BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS, + }; + } + + if (doAllSelectedRulesHaveConflicts) { + return { + selectedRulesButtonTooltip: i18n.BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS, + allRulesButtonTooltip: undefined, + }; + } + + return { + selectedRulesButtonTooltip: undefined, + allRulesButtonTooltip: undefined, + }; +}; + +function isAllRuleHaveConflicts(rules: Array<{ diff: { num_fields_with_conflicts: number } }>) { + return rules.every((rule) => rule.diff.num_fields_with_conflicts > 0); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 6ec9ffdd02e6..cbb0350da173 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -9,35 +9,36 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; +import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; +import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine'; import type { RulesUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; import { RuleUpgradeConflictsResolverTab } from '../../../../rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab'; import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; -import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; -import { useBoolState } from '../../../../../common/hooks/use_bool_state'; -import { affectedJobIds } from '../../../../../detections/components/callouts/ml_job_compatibility_callout/affected_job_ids'; import type { RuleResponse, RuleSignatureId, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; -import { - usePerformUpgradeAllRules, - usePerformUpgradeSpecificRules, -} from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; +import { usePerformUpgradeSpecificRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_upgrade'; import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade'; -import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; -import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal'; +import { MlJobUpgradeModal } from './modals/ml_job_upgrade_modal'; +import { UpgradeConflictsModal } from './modals/upgrade_conflicts_modal'; import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; import * as i18n from './translations'; import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; +import { useMlJobUpgradeModal, useUpgradeConflictsModal } from './use_upgrade_modals'; export interface UpgradePrebuiltRulesTableState { + /** + * Rule upgrade state (all rules available for upgrade) + */ + ruleUpgradeInfos: RuleUpgradeInfoForReview[]; /** * Rule upgrade state after applying `filterOptions` */ @@ -90,7 +91,6 @@ export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; export interface UpgradePrebuiltRulesTableActions { reFetchRules: () => void; upgradeRules: (ruleIds: RuleSignatureId[]) => void; - upgradeAllRules: () => void; setFilterOptions: Dispatch>; openRulePreview: (ruleId: string) => void; } @@ -118,6 +118,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ tags: [], ruleSource: [], }); + const { addError } = useAppToasts(); const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); @@ -142,21 +143,41 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const { rulesUpgradeState, setRuleFieldResolvedValue } = usePrebuiltRulesUpgradeState(filteredRuleUpgradeInfos); - // Wrapper to add confirmation modal for users who may be running older ML Jobs that would - // be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121 - const [isUpgradeModalVisible, showUpgradeModal, hideUpgradeModal] = useBoolState(false); - const { loading: loadingJobs, jobs } = useInstalledSecurityJobs(); - const legacyJobsInstalled = jobs.filter((job) => affectedJobIds.includes(job.id)); + const { + isVisible: isLegacyMLJobsModalVisible, + legacyJobsInstalled, + confirmLegacyMLJobs, + handleConfirm: handleLegacyMLJobsConfirm, + handleCancel: handleLegacyMLJobsCancel, + loadingJobs, + } = useMlJobUpgradeModal(); - const [confirmUpgrade, handleUpgradeConfirm, handleUpgradeCancel] = useAsyncConfirmation({ - onInit: showUpgradeModal, - onFinish: hideUpgradeModal, - }); + const { + isVisible: isConflictsModalVisible, + confirmConflictsUpgrade, + handleConfirm: handleConflictsConfirm, + handleCancel: handleConflictsCancel, + } = useUpgradeConflictsModal(); - const shouldConfirmUpgrade = legacyJobsInstalled.length > 0; + const shouldConfirmMLJobs = legacyJobsInstalled.length > 0; + const getRulesWithConflicts = useCallback( + (ruleIds?: RuleSignatureId[]) => { + const rulesToUpgrade = + ruleIds?.map((ruleId) => { + const rule = rulesUpgradeState[ruleId]; + invariant(rule, `Rule with ID ${ruleId} not found.`); - const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules(); - const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); + return rule; + }) ?? []; + + return rulesToUpgrade.filter((rule) => rule.diff.num_fields_with_conflicts > 0); + }, + [rulesUpgradeState] + ); + + const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules({ + pickVersion: isPrebuiltRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + }); const upgradeRules = useCallback( async (ruleIds: RuleSignatureId[]) => { @@ -169,32 +190,47 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ })); setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); try { - if (shouldConfirmUpgrade && !(await confirmUpgrade())) { + // Handle MLJobs modal + if (shouldConfirmMLJobs && !(await confirmLegacyMLJobs())) { return; } - await upgradeSpecificRulesRequest(rulesToUpgrade); + + // Handle Rule Upgrades modal + const rulesWithConflicts = getRulesWithConflicts(ruleIds); + if ( + isPrebuiltRulesCustomizationEnabled && + rulesWithConflicts.length > 0 && + !(await confirmConflictsUpgrade()) + ) { + return; + } + + // Prepare payload for upgrade with rules with no conflicts + const ruleIdsWithConflicts = new Set(rulesWithConflicts.map((rule) => rule.rule_id)); + const rulesToUpgradeWithNoConflicts = isPrebuiltRulesCustomizationEnabled + ? rulesToUpgrade.filter((rule) => !ruleIdsWithConflicts.has(rule.rule_id)) + : rulesToUpgrade; + await upgradeSpecificRulesRequest(rulesToUpgradeWithNoConflicts); + } catch (err) { + addError(err, { title: i18n.UPDATE_ERROR }); } finally { setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)) ); } }, - [confirmUpgrade, shouldConfirmUpgrade, rulesUpgradeState, upgradeSpecificRulesRequest] + [ + confirmLegacyMLJobs, + confirmConflictsUpgrade, + shouldConfirmMLJobs, + getRulesWithConflicts, + rulesUpgradeState, + upgradeSpecificRulesRequest, + isPrebuiltRulesCustomizationEnabled, + addError, + ] ); - const upgradeAllRules = useCallback(async () => { - // Unselect all rules so that the table doesn't show the "bulk actions" bar - setLoadingRules((prev) => [...prev, ...ruleUpgradeInfos.map((r) => r.rule_id)]); - try { - if (shouldConfirmUpgrade && !(await confirmUpgrade())) { - return; - } - await upgradeAllRulesRequest(); - } finally { - setLoadingRules([]); - } - }, [confirmUpgrade, ruleUpgradeInfos, shouldConfirmUpgrade, upgradeAllRulesRequest]); - const ruleActionsFactory = useCallback( (rule: RuleResponse, closeRulePreview: () => void) => ( ({ reFetchRules: refetch, upgradeRules, - upgradeAllRules, setFilterOptions, openRulePreview, }), - [refetch, upgradeRules, upgradeAllRules, openRulePreview] + [refetch, upgradeRules, openRulePreview] ); const providerValue = useMemo(() => { return { state: { + ruleUpgradeInfos, rulesUpgradeState, hasRulesToUpgrade: isFetched && ruleUpgradeInfos.length > 0, filterOptions, @@ -343,11 +379,17 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return ( <> - {isUpgradeModalVisible && ( + {isLegacyMLJobsModalVisible && ( + )} + {isConflictsModalVisible && ( + )} {children} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_modals.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_modals.tsx new file mode 100644 index 000000000000..3e20d914dabc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_modals.tsx @@ -0,0 +1,45 @@ +/* + * 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 { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; +import { useBoolState } from '../../../../../common/hooks/use_bool_state'; +import { affectedJobIds } from '../../../../../../common/machine_learning/affected_job_ids'; +import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; + +export const useMlJobUpgradeModal = () => { + const [isVisible, showModal, hideModal] = useBoolState(false); + const { loading: loadingJobs, jobs } = useInstalledSecurityJobs(); + const legacyJobsInstalled = jobs.filter((job) => affectedJobIds.includes(job.id)); + const [confirmLegacyMLJobs, handleConfirm, handleCancel] = useAsyncConfirmation({ + onInit: showModal, + onFinish: hideModal, + }); + + return { + isVisible, + legacyJobsInstalled, + confirmLegacyMLJobs, + handleConfirm, + handleCancel, + loadingJobs, + }; +}; + +export const useUpgradeConflictsModal = () => { + const [isVisible, showModal, hideModal] = useBoolState(false); + const [confirmConflictsUpgrade, handleConfirm, handleCancel] = useAsyncConfirmation({ + onInit: showModal, + onFinish: hideModal, + }); + + return { + isVisible, + confirmConflictsUpgrade, + handleConfirm, + handleCancel, + }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx index 579f571f80e7..c2b1f8e3dbe4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -160,15 +160,21 @@ const createUpgradeButtonColumn = ( /> ); + const tooltipContent = isDisabledByConflicts + ? i18n.UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS + : undefined; + return ( - upgradeRules([ruleId])} - data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`} - > - {isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON} - + + upgradeRules([ruleId])} + data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`} + > + {isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON} + + ); }, width: '10%', diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.tsx index df4eb0a24968..92d42b70d240 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/ml_job_compatibility_callout/index.tsx @@ -10,7 +10,7 @@ import React, { memo } from 'react'; import type { CallOutMessage } from '../../../../common/components/callouts'; import { CallOutSwitcher } from '../../../../common/components/callouts'; import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; -import { affectedJobIds } from './affected_job_ids'; +import { affectedJobIds } from '../../../../../common/machine_learning/affected_job_ids'; import * as i18n from './translations'; const mlJobCompatibilityCalloutMessage: CallOutMessage = { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index b6a3bbdc0ad9..cb0570855dab 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1435,6 +1435,13 @@ export const UPDATE_RULE_BUTTON = i18n.translate( } ); +export const UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.button.conflicts', + { + defaultMessage: 'Rule has conflicts. Resolve them manually.', + } +); + export const GO_BACK_TO_RULES_TABLE_BUTTON = i18n.translate( 'xpack.securitySolution.addRules.goBackToRulesTableButton', { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts index b25320e1131e..7f9f66d1019c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts @@ -26,6 +26,7 @@ import { getValueForField } from './get_value_for_field'; interface CreateModifiedPrebuiltRuleAssetsProps { upgradeableRules: RuleTriad[]; requestBody: PerformRuleUpgradeRequestBody; + prebuiltRulesCustomizationEnabled: boolean; } interface ProcessedRules { @@ -36,9 +37,13 @@ interface ProcessedRules { export const createModifiedPrebuiltRuleAssets = ({ upgradeableRules, requestBody, + prebuiltRulesCustomizationEnabled, }: CreateModifiedPrebuiltRuleAssetsProps) => { return withSecuritySpanSync(createModifiedPrebuiltRuleAssets.name, () => { - const { pick_version: globalPickVersion = PickVersionValuesEnum.MERGED, mode } = requestBody; + const defaultPickVersion = prebuiltRulesCustomizationEnabled + ? PickVersionValuesEnum.MERGED + : PickVersionValuesEnum.TARGET; + const { pick_version: globalPickVersion = defaultPickVersion, mode } = requestBody; const { modifiedPrebuiltRuleAssets, processingErrors } = upgradeableRules.reduce( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts index 085c41db3a5d..c8b5d459f678 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -25,8 +25,12 @@ import { PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS } from '../../constants'; import { getUpgradeableRules } from './get_upgradeable_rules'; import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload'; import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; +import type { ConfigType } from '../../../../../config'; -export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { +export const performRuleUpgradeRoute = ( + router: SecuritySolutionPluginRouter, + config: ConfigType +) => { router.versioned .post({ access: 'internal', @@ -75,10 +79,12 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => mode, }); + const { prebuiltRulesCustomizationEnabled } = config.experimentalFeatures; const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets( { upgradeableRules, requestBody: request.body, + prebuiltRulesCustomizationEnabled, } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts index c9871f86a43e..2a2cf8823763 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/register_routes.ts @@ -6,7 +6,7 @@ */ import type { SecuritySolutionPluginRouter } from '../../../../types'; - +import type { ConfigType } from '../../../../config'; import { getPrebuiltRulesAndTimelinesStatusRoute } from './get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route'; import { getPrebuiltRulesStatusRoute } from './get_prebuilt_rules_status/get_prebuilt_rules_status_route'; import { installPrebuiltRulesAndTimelinesRoute } from './install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route'; @@ -16,7 +16,10 @@ import { performRuleInstallationRoute } from './perform_rule_installation/perfor import { performRuleUpgradeRoute } from './perform_rule_upgrade/perform_rule_upgrade_route'; import { bootstrapPrebuiltRulesRoute } from './bootstrap_prebuilt_rules/bootstrap_prebuilt_rules'; -export const registerPrebuiltRulesRoutes = (router: SecuritySolutionPluginRouter) => { +export const registerPrebuiltRulesRoutes = ( + router: SecuritySolutionPluginRouter, + config: ConfigType +) => { // Legacy endpoints that we're going to deprecate getPrebuiltRulesAndTimelinesStatusRoute(router); installPrebuiltRulesAndTimelinesRoute(router); @@ -24,7 +27,7 @@ export const registerPrebuiltRulesRoutes = (router: SecuritySolutionPluginRouter // New endpoints for the rule upgrade and installation workflows getPrebuiltRulesStatusRoute(router); performRuleInstallationRoute(router); - performRuleUpgradeRoute(router); + performRuleUpgradeRoute(router, config); reviewRuleInstallationRoute(router); reviewRuleUpgradeRoute(router); bootstrapPrebuiltRulesRoute(router); diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 8fb74afc770b..f7d5ce7afb3d 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -81,7 +81,7 @@ export const initRoutes = ( ) => { registerFleetIntegrationsRoutes(router); registerLegacyRuleActionsRoutes(router, logger); - registerPrebuiltRulesRoutes(router); + registerPrebuiltRulesRoutes(router, config); registerRuleExceptionsRoutes(router); registerManageExceptionsRoutes(router); registerRuleManagementRoutes(router, config, ml, logger); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_with_prebuilt_rule_customization.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_with_prebuilt_rule_customization.cy.ts index 52b050f46c06..7b487b3d7fdb 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_with_prebuilt_rule_customization.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_with_prebuilt_rule_customization.cy.ts @@ -5,23 +5,34 @@ * 2.0. */ -import { patchRule } from '../../../../tasks/api_calls/rules'; import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { MODIFIED_RULE_BADGE, + NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE, + RULES_UPDATES_TAB, RULES_UPDATES_TABLE, + SELECT_ALL_RULES_ON_PAGE_CHECKBOX, + UPGRADE_ALL_RULES_BUTTON, + UPGRADE_SELECTED_RULES_BUTTON, + getUpgradeSingleRuleButtonByRuleId, } from '../../../../screens/alerts_detection_rules'; +import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; import { - installPrebuiltRuleAssets, createAndInstallMockedPrebuiltRules, + installPrebuiltRuleAssets, } from '../../../../tasks/api_calls/prebuilt_rules'; +import { patchRule } from '../../../../tasks/api_calls/rules'; import { resetRulesTableState } from '../../../../tasks/common'; import { login } from '../../../../tasks/login'; import { + assertRuleUpgradeConflictsModalShown, + assertRuleUpgradeSuccessToastShown, assertRulesNotPresentInRuleUpdatesTable, assertRulesPresentInRuleUpdatesTable, + assertUpgradeRequestIsComplete, clickRuleUpdatesTab, + clickUpgradeRuleWithoutConflicts, filterPrebuiltRulesUpdateTableByRuleCustomization, } from '../../../../tasks/prebuilt_rules'; import { visitRulesManagementTable } from '../../../../tasks/rules_management'; @@ -42,6 +53,90 @@ describe( }, () => { + describe('Upgrade of prebuilt rules without conflicts', () => { + const RULE_1_ID = 'rule_1'; + const RULE_2_ID = 'rule_2'; + const OUTDATED_RULE_1 = createRuleAssetSavedObject({ + name: 'Outdated rule 1', + rule_id: RULE_1_ID, + version: 1, + }); + const UPDATED_RULE_1 = createRuleAssetSavedObject({ + name: 'Updated rule 1', + rule_id: RULE_1_ID, + version: 2, + }); + const OUTDATED_RULE_2 = createRuleAssetSavedObject({ + name: 'Outdated rule 2', + rule_id: RULE_2_ID, + version: 1, + }); + const UPDATED_RULE_2 = createRuleAssetSavedObject({ + name: 'Updated rule 2', + rule_id: RULE_2_ID, + version: 2, + }); + beforeEach(() => { + login(); + resetRulesTableState(); + deleteAlertsAndRules(); + cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/upgrade/_perform').as( + 'updatePrebuiltRules' + ); + /* Create a new rule and install it */ + createAndInstallMockedPrebuiltRules([OUTDATED_RULE_1, OUTDATED_RULE_2]); + /* Create a second version of the rule, making it available for update */ + installPrebuiltRuleAssets([UPDATED_RULE_1, UPDATED_RULE_2]); + + visitRulesManagementTable(); + clickRuleUpdatesTab(); + }); + + it('should upgrade prebuilt rules one by one', () => { + // Attempt to upgrade rule + cy.get( + getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id) + ).click(); + // Wait for request to complete + assertUpgradeRequestIsComplete([OUTDATED_RULE_1]); + + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_1]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_1]); + }); + + it('should upgrade multiple selected prebuilt rules by selecting them individually', () => { + selectRulesByName([ + OUTDATED_RULE_1['security-rule'].name, + OUTDATED_RULE_2['security-rule'].name, + ]); + cy.get(UPGRADE_SELECTED_RULES_BUTTON).click(); + assertUpgradeRequestIsComplete([OUTDATED_RULE_1, OUTDATED_RULE_2]); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_1, OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); + }); + + it('should upgrade multiple selected prebuilt rules by selecting all in page', () => { + cy.get(SELECT_ALL_RULES_ON_PAGE_CHECKBOX).click(); + cy.get(UPGRADE_SELECTED_RULES_BUTTON).click(); + assertUpgradeRequestIsComplete([OUTDATED_RULE_1, OUTDATED_RULE_2]); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_1, OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); + }); + + it('should upgrade all rules with available upgrades at once', () => { + cy.get(UPGRADE_ALL_RULES_BUTTON).click(); + assertUpgradeRequestIsComplete([OUTDATED_RULE_1, OUTDATED_RULE_2]); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_1, OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_1, OUTDATED_RULE_2]); + }); + + it('should display an empty screen when all rules with available updates have been upgraded', () => { + cy.get(UPGRADE_ALL_RULES_BUTTON).click(); + cy.get(RULES_UPDATES_TAB).should('not.exist'); + cy.get(NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE).should('exist'); + }); + }); + describe('Upgrade of prebuilt rules with conflicts', () => { const RULE_1_ID = 'rule_1'; const RULE_2_ID = 'rule_2'; @@ -104,6 +199,60 @@ describe( // Verify only rules with non-customized rule sources are displayed assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_2]); cy.get(patchedName).should('not.exist'); + it('should upgrade prebuilt rules without conflicts one by one', () => { + cy.get( + getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_2['security-rule'].rule_id) + ).click(); + // Wait for request to complete + assertUpgradeRequestIsComplete([OUTDATED_RULE_2]); + + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_2]); + }); + + it('should disable individual upgrade button for prebuilt rules with conflicts one by one', () => { + // Button should be disabled because of conflicts + expect( + cy + .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id)) + .should('be.disabled') + ); + }); + + it('should warn about rules with conflicts not being updated when multiple rules are individually selected for update', () => { + selectRulesByName([patchedName, OUTDATED_RULE_2['security-rule'].name]); + cy.get(UPGRADE_SELECTED_RULES_BUTTON).click(); + assertRuleUpgradeConflictsModalShown(); + clickUpgradeRuleWithoutConflicts(); + // Assert that only rules without conflicts are updated and the other remains in the table + assertUpgradeRequestIsComplete([OUTDATED_RULE_2]); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_2]); + cy.get(RULES_UPDATES_TABLE).contains(patchedName); + }); + + it('should warn about rules with conflicts not being updated when all rules in page are selected', () => { + cy.get(SELECT_ALL_RULES_ON_PAGE_CHECKBOX).click(); + cy.get(UPGRADE_SELECTED_RULES_BUTTON).click(); + assertRuleUpgradeConflictsModalShown(); + clickUpgradeRuleWithoutConflicts(); + // Assert that only rules without conflicts are updated and the other remains in the table + assertUpgradeRequestIsComplete([OUTDATED_RULE_2]); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_2]); + cy.get(RULES_UPDATES_TABLE).contains(patchedName); + }); + + it('should warn about rules with conflicts not being updated when all rules with available upgrades are upgraded at once', () => { + cy.get(UPGRADE_ALL_RULES_BUTTON).click(); + assertRuleUpgradeConflictsModalShown(); + clickUpgradeRuleWithoutConflicts(); + // Assert that only rules without conflicts are updated and the other remains in the table + assertUpgradeRequestIsComplete([OUTDATED_RULE_2]); + assertRuleUpgradeSuccessToastShown([OUTDATED_RULE_2]); + assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_2]); + cy.get(RULES_UPDATES_TABLE).contains(patchedName); + }); }); }); } diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts index 4b11a4624c3e..dbe907a9040f 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts @@ -10,3 +10,5 @@ export const RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON = export const RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL = '[data-test-subj="rule-customization-filter-popover"]'; + +export const RULE_UPGRADE_CONFLICTS_MODAL = '[data-test-subj="upgradeConflictsModal"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts index d4148d5e632a..0dd833810f9b 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts @@ -21,6 +21,7 @@ import { RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON, RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL, } from '../screens/rule_updates'; +import { RULE_UPGRADE_CONFLICTS_MODAL } from '../screens/rule_updates'; export const clickAddElasticRulesButton = () => { cy.get(ADD_ELASTIC_RULES_BTN).click(); @@ -160,3 +161,11 @@ export const filterPrebuiltRulesUpdateTableByRuleCustomization = (text: string) cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL).contains(text).click(); cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON).click(); }; + +export const assertRuleUpgradeConflictsModalShown = () => { + cy.get(RULE_UPGRADE_CONFLICTS_MODAL).should('be.visible'); +}; + +export const clickUpgradeRuleWithoutConflicts = () => { + cy.get(RULE_UPGRADE_CONFLICTS_MODAL).contains('button', 'Update').click(); +};