From f2077dbb3108833e76a2f1834a76c3266e22f2ac Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 6 Mar 2025 18:39:58 +0100 Subject: [PATCH] [Security Solution] Allow bulk upgrade rules with solvable conflicts (#213285) **Partially addresses:** https://github.com/elastic/kibana/issues/210358 ## Summary This PR implements functionality allowing users to bulk upgrade rules with solvable conflicts. ## Details The main focus of this PR is to allow users to bulk upgrade rules with solvable conflicts. To achieve that the following was done - `upgrade/_perform` dry run functionality was extended to take into account rule upgrade specifiers with resolved value - `upgrade/_perform`'s `on_conflict` param was extended with `UPGRADE_SOLVABLE` to allow bulk upgrading rules with solvable conflicts - UI logic updated accordingly to display rule upgrade modal when users have to make a choice to upgrade only rules without conflicts or upgrade also rules with solvable conflicts - conflict state badges were added to the rule upgrade table It includes changes from https://github.com/elastic/kibana/pull/213027 with some modifications. ## Screenshots Screenshot 2025-03-06 at 12 13 04 Screenshot 2025-03-06 at 12 13 30 Screenshot 2025-03-06 at 12 13 51 Screenshot 2025-03-06 at 12 14 04 Screenshot 2025-03-06 at 12 14 17 --- .../perform_rule_upgrade_route.ts | 23 ++- .../pages/rule_details/index.tsx | 2 +- .../hooks/use_prebuilt_rules_upgrade.tsx | 136 +++++++++++------- .../rule_upgrade_state.ts | 9 +- .../rules_table/use_async_confirmation.ts | 19 +-- .../components/rules_table/rules_tables.tsx | 2 +- .../upgrade_prebuilt_rules_table_buttons.tsx | 4 +- .../use_prebuilt_rules_upgrade_state.ts | 20 +++ ...e_upgrade_prebuilt_rules_table_columns.tsx | 42 +++++- .../translations.tsx | 134 ++++++++++++++++- .../upgrade_modal.tsx | 123 +++++++++++++--- .../use_upgrade_modal.tsx | 36 ++++- .../detection_engine/rules/translations.ts | 41 +++++- .../create_upgradeable_rules_payload.ts | 25 +++- .../get_value_for_field.ts | 2 + .../get_value_from_rule_version.ts | 16 ++- .../perform_rule_upgrade_handler.ts | 52 ++++++- 17 files changed, 574 insertions(+), 112 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts index de3bb4fc27e1a..ad96671b69ae5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -8,7 +8,7 @@ import { z } from '@kbn/zod'; import { mapValues } from 'lodash'; import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen'; -import { AggregatedPrebuiltRuleError, DiffableAllFields } from '../model'; +import { AggregatedPrebuiltRuleError, DiffableAllFields, ThreeWayDiffConflict } from '../model'; import { RuleSignatureId, RuleVersion } from '../../model'; import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter'; @@ -113,7 +113,7 @@ export const RuleUpgradeSpecifier = z.object({ }); export type UpgradeConflictResolution = z.infer; -export const UpgradeConflictResolution = z.enum(['SKIP', 'OVERWRITE']); +export const UpgradeConflictResolution = z.enum(['SKIP', 'UPGRADE_SOLVABLE']); export type UpgradeConflictResolutionEnum = typeof UpgradeConflictResolution.enum; export const UpgradeConflictResolutionEnum = UpgradeConflictResolution.enum; @@ -140,12 +140,25 @@ export const SkipRuleUpgradeReason = z.enum(['RULE_UP_TO_DATE', 'CONFLICT']); export type SkipRuleUpgradeReasonEnum = typeof SkipRuleUpgradeReason.enum; export const SkipRuleUpgradeReasonEnum = SkipRuleUpgradeReason.enum; -export type SkippedRuleUpgrade = z.infer; -export const SkippedRuleUpgrade = z.object({ +export type RuleUpToDateSkipReason = z.infer; +export const RuleUpToDateSkipReason = z.object({ + reason: z.literal(SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE), + rule_id: z.string(), +}); + +export type UpgradeConflictSkipReason = z.infer; +export const UpgradeConflictSkipReason = z.object({ + reason: z.literal(SkipRuleUpgradeReasonEnum.CONFLICT), rule_id: z.string(), - reason: SkipRuleUpgradeReason, + conflict: z.nativeEnum(ThreeWayDiffConflict), }); +export type SkippedRuleUpgrade = z.infer; +export const SkippedRuleUpgrade = z.discriminatedUnion('reason', [ + RuleUpToDateSkipReason, + UpgradeConflictSkipReason, +]); + export type PerformRuleUpgradeResponseBody = z.infer; export const PerformRuleUpgradeResponseBody = z.object({ summary: z.object({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 01da57abb4943..2724850a4ec84 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -575,7 +575,7 @@ const RuleDetailsPageComponent: React.FC = ({ handleDeletionConfirm()} confirmButtonText={ruleI18n.DELETE_CONFIRMATION_CONFIRM} cancelButtonText={ruleI18n.DELETE_CONFIRMATION_CANCEL} buttonColor="danger" diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx index c7a292d580908..3936ff443e511 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx @@ -15,16 +15,23 @@ import { useIsUpgradingSecurityPackages } from '../logic/use_upgrade_security_pa import { usePrebuiltRulesCustomizationStatus } from '../logic/prebuilt_rules/use_prebuilt_rules_customization_status'; import { usePerformUpgradeRules } from '../logic/prebuilt_rules/use_perform_rule_upgrade'; import { usePrebuiltRulesUpgradeReview } from '../logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; -import type { - FindRulesSortField, - RuleFieldsToUpgrade, - RuleResponse, - RuleSignatureId, - RuleUpgradeSpecifier, +import type { PerformRuleUpgradeRequestBody } from '../../../../common/api/detection_engine'; +import { + type FindRulesSortField, + type RuleFieldsToUpgrade, + type RuleResponse, + type RuleSignatureId, + type RuleUpgradeSpecifier, + ThreeWayDiffConflict, + SkipRuleUpgradeReasonEnum, + UpgradeConflictResolutionEnum, } from '../../../../common/api/detection_engine'; import { usePrebuiltRulesUpgradeState } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; import { useOutdatedMlJobsUpgradeModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_ml_jobs_upgrade_modal'; -import { useUpgradeWithConflictsModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal'; +import { + ConfirmRulesUpgrade, + useUpgradeWithConflictsModal, +} from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal'; import * as ruleDetailsI18n from '../components/rule_details/translations'; import * as i18n from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations'; import { UpgradeFlyoutSubHeader } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader'; @@ -36,6 +43,7 @@ import { RuleDiffTab } from '../components/rule_details/rule_diff_tab'; import { useRulePreviewFlyout } from '../../rule_management_ui/components/rules_table/use_rule_preview_flyout'; import type { UpgradePrebuiltRulesSortingOptions } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../../rule_management_ui/components/rules_table/constants'; +import type { RulesConflictStats } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/upgrade_modal'; const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; @@ -100,26 +108,18 @@ export function usePrebuiltRulesUpgrade({ const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); + const upgradeRulesWithDryRun = useRulesUpgradeWithDryRun(confirmConflictsUpgrade); const upgradeRulesToResolved = useCallback( async (ruleIds: RuleSignatureId[]) => { - const conflictRuleIdsSet = new Set( - ruleIds.filter( - (ruleId) => - rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 && - rulesUpgradeState[ruleId].hasUnresolvedConflicts - ) - ); - - const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId)); - const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({ + const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({ rule_id: ruleId, version: rulesUpgradeState[ruleId].target_rule.version, revision: rulesUpgradeState[ruleId].revision, fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]), })); - setLoadingRules((prev) => [...prev, ...upgradingRuleIds]); + setLoadingRules((prev) => [...prev, ...ruleIds]); try { // Handle MLJobs modal @@ -127,11 +127,7 @@ export function usePrebuiltRulesUpgrade({ return; } - if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) { - return; - } - - await upgradeRulesRequest({ + await upgradeRulesWithDryRun({ mode: 'SPECIFIC_RULES', pick_version: 'MERGED', rules: ruleUpgradeSpecifiers, @@ -139,7 +135,7 @@ export function usePrebuiltRulesUpgrade({ } catch { // Error is handled by the mutation's onError callback, so no need to do anything here } finally { - const upgradedRuleIdsSet = new Set(upgradingRuleIds); + const upgradedRuleIdsSet = new Set(ruleIds); if (onUpgrade) { onUpgrade(); @@ -148,13 +144,7 @@ export function usePrebuiltRulesUpgrade({ setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); } }, - [ - rulesUpgradeState, - confirmLegacyMLJobs, - confirmConflictsUpgrade, - upgradeRulesRequest, - onUpgrade, - ] + [rulesUpgradeState, confirmLegacyMLJobs, upgradeRulesWithDryRun, onUpgrade] ); const upgradeRulesToTarget = useCallback( @@ -213,27 +203,10 @@ export function usePrebuiltRulesUpgrade({ return; } - const dryRunResults = await upgradeRulesRequest({ - mode: 'ALL_RULES', - pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', - filter, - dry_run: true, - on_conflict: 'SKIP', - }); - - const hasConflicts = dryRunResults.results.skipped.some( - (skippedRule) => skippedRule.reason === 'CONFLICT' - ); - - if (hasConflicts && !(await confirmConflictsUpgrade())) { - return; - } - - await upgradeRulesRequest({ + await upgradeRulesWithDryRun({ mode: 'ALL_RULES', pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', filter, - on_conflict: 'SKIP', }); } catch { // Error is handled by the mutation's onError callback, so no need to do anything here @@ -242,11 +215,10 @@ export function usePrebuiltRulesUpgrade({ } }, [ upgradeableRules, + upgradeRulesWithDryRun, confirmLegacyMLJobs, - upgradeRulesRequest, isRulesCustomizationEnabled, filter, - confirmConflictsUpgrade, ]); const subHeaderFactory = useCallback( @@ -405,6 +377,68 @@ export function usePrebuiltRulesUpgrade({ }; } +/** + * Upgrades rules in two steps + * - first fires a dry run request to check for rule upgrade conflicts. If there are conflicts + * it calls `confirmConflictsUpgrade()` and await its result. + * - second it either fires a request to upgrade rules or exits depending on user's choice + */ +function useRulesUpgradeWithDryRun( + confirmConflictsUpgrade: ( + conflictsStats: RulesConflictStats + ) => Promise +) { + const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); + + return useCallback( + async (requestParams: PerformRuleUpgradeRequestBody) => { + const dryRunResults = await upgradeRulesRequest({ + ...requestParams, + dry_run: true, + on_conflict: UpgradeConflictResolutionEnum.SKIP, + }); + + const numOfRulesWithSolvableConflicts = dryRunResults.results.skipped.filter( + (x) => + x.reason === SkipRuleUpgradeReasonEnum.CONFLICT && + x.conflict === ThreeWayDiffConflict.SOLVABLE + ).length; + const numOfRulesWithNonSolvableConflicts = dryRunResults.results.skipped.filter( + (x) => + x.reason === SkipRuleUpgradeReasonEnum.CONFLICT && + x.conflict === ThreeWayDiffConflict.NON_SOLVABLE + ).length; + + if (numOfRulesWithSolvableConflicts === 0 && numOfRulesWithNonSolvableConflicts === 0) { + // There are no rule with conflicts + await upgradeRulesRequest({ + ...requestParams, + on_conflict: UpgradeConflictResolutionEnum.SKIP, + }); + } else { + const result = await confirmConflictsUpgrade({ + numOfRulesWithoutConflicts: dryRunResults.results.updated.length, + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, + }); + + if (!result) { + return; + } + + await upgradeRulesRequest({ + ...requestParams, + on_conflict: + result === ConfirmRulesUpgrade.WithSolvableConflicts + ? UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE + : UpgradeConflictResolutionEnum.SKIP, + }); + } + }, + [upgradeRulesRequest, confirmConflictsUpgrade] + ); +} + function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade { const ruleFieldsToUpgrade: Record = {}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts index 45428fa1bb387..496d31130cbcf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state.ts @@ -5,10 +5,17 @@ * 2.0. */ -import { type RuleUpgradeInfoForReview } from '../../../../../common/api/detection_engine'; +import { + type ThreeWayDiffConflict, + type RuleUpgradeInfoForReview, +} from '../../../../../common/api/detection_engine'; import type { FieldsUpgradeState } from './fields_upgrade_state'; export interface RuleUpgradeState extends RuleUpgradeInfoForReview { + /** + * Original rule conflict state calculated from fields diff. + */ + conflict: ThreeWayDiffConflict; /** * Stores a record of customizable field names mapped to field upgrade state. */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/use_async_confirmation.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/use_async_confirmation.ts index 3042906f87cf3..808dabd6543bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/use_async_confirmation.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/use_async_confirmation.ts @@ -7,9 +7,9 @@ import { useCallback, useRef } from 'react'; -type UseAsyncConfirmationReturn = [ - initConfirmation: () => Promise, - confirm: () => void, +type UseAsyncConfirmationReturn = [ + initConfirmation: () => Promise, + confirm: (result?: ConfirmResult) => void, cancel: () => void ]; @@ -19,14 +19,15 @@ interface UseAsyncConfirmationArgs { } // TODO move to common hooks -export const useAsyncConfirmation = ({ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const useAsyncConfirmation = ({ onInit, onFinish, -}: UseAsyncConfirmationArgs): UseAsyncConfirmationReturn => { - const confirmationPromiseRef = useRef<(result: boolean) => void>(); +}: UseAsyncConfirmationArgs): UseAsyncConfirmationReturn => { + const confirmationPromiseRef = useRef<(result: ConfirmResult | boolean) => void>(); - const confirm = useCallback(() => { - confirmationPromiseRef.current?.(true); + const confirm = useCallback((result?: ConfirmResult) => { + confirmationPromiseRef.current?.(result ?? true); }, []); const cancel = useCallback(() => { @@ -36,7 +37,7 @@ export const useAsyncConfirmation = ({ const initConfirmation = useCallback(() => { onInit(); - return new Promise((resolve) => { + return new Promise((resolve) => { confirmationPromiseRef.current = resolve; }).finally(() => { onFinish(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx index 4742d3d57831c..f68ab91734f80 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx @@ -275,7 +275,7 @@ export const RulesTables = React.memo(({ selectedTab }) => { : i18n.BULK_DELETE_CONFIRMATION_TITLE } onCancel={handleDeletionCancel} - onConfirm={handleDeletionConfirm} + onConfirm={() => handleDeletionConfirm()} confirmButtonText={i18n.DELETE_CONFIRMATION_CONFIRM} cancelButtonText={i18n.DELETE_CONFIRMATION_CANCEL} buttonColor="danger" diff --git a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx index 5498e8961c06a..a1b9a36935c96 100644 --- a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx @@ -36,7 +36,9 @@ export const UpgradePrebuiltRulesTableButtons = ({ const doAllSelectedRulesHaveConflicts = isRulesCustomizationEnabled && - selectedRules.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts); + selectedRules.every( + ({ hasNonSolvableUnresolvedConflicts }) => hasNonSolvableUnresolvedConflicts + ); const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({ canUserEditRules, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts index 2a20068ab8639..663a39a80de5d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts @@ -140,6 +140,7 @@ export function usePrebuiltRulesUpgradeState( state[ruleUpgradeInfo.rule_id] = { ...ruleUpgradeInfo, + conflict: getWorstConflictLevelAmongFields(ruleUpgradeInfo.diff.fields), fieldsUpgradeState, hasUnresolvedConflicts: isRulesCustomizationEnabled ? hasRuleTypeChange || hasFieldConflicts @@ -212,3 +213,22 @@ function calcFieldsState( return fieldsState; } + +function getWorstConflictLevelAmongFields( + fieldsDiff: FieldsDiff> +): ThreeWayDiffConflict { + let mostSevereFieldConflict = ThreeWayDiffConflict.NONE; + + for (const { conflict } of Object.values<{ conflict: ThreeWayDiffConflict }>(fieldsDiff)) { + if (conflict === ThreeWayDiffConflict.NON_SOLVABLE) { + // return early as there is no higher severity + return ThreeWayDiffConflict.NON_SOLVABLE; + } + + if (conflict === ThreeWayDiffConflict.SOLVABLE) { + mostSevereFieldConflict = ThreeWayDiffConflict.SOLVABLE; + } + } + + return mostSevereFieldConflict; +} diff --git a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx index 014f107687c12..7961920507d26 100644 --- a/x-pack/solutions/security/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/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -15,6 +15,7 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useMemo } from 'react'; +import { ThreeWayDiffConflict } from '../../../../../../common/api/detection_engine'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state'; import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name'; import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; @@ -142,6 +143,43 @@ const MODIFIED_COLUMN: TableColumn = { truncateText: true, }; +const CONFLICT_COLUMN: TableColumn = { + field: 'conflict', + name: , + align: 'center', + render: (conflict: ThreeWayDiffConflict) => { + switch (conflict) { + case ThreeWayDiffConflict.SOLVABLE: + return ( + + + {i18n.SOLVABLE_CONFLICT_LABEL} + + + ); + + case ThreeWayDiffConflict.NON_SOLVABLE: + return ( + + + {i18n.NON_SOLVABLE_CONFLICT_LABEL} + + + ); + } + }, + width: '170px', + truncateText: true, +}; + const createUpgradeButtonColumn = ( upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'], openRulePreview: UpgradePrebuiltRulesTableActions['openRulePreview'], @@ -167,6 +205,7 @@ const createUpgradeButtonColumn = ( return ( openRulePreview(ruleId)} @@ -217,6 +256,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { () => [ RULE_NAME_COLUMN, ...(shouldShowModifiedColumn ? [MODIFIED_COLUMN] : []), + CONFLICT_COLUMN, ...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []), TAGS_COLUMN, { @@ -238,7 +278,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { sortable: ({ current_rule: { severity } }: RuleUpgradeState) => getNormalizedSeverity(severity), truncateText: true, - width: '12%', + width: '10%', }, ...(hasCRUDPermissions ? [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/translations.tsx index bc5738b879cc2..8febd25e93e31 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/translations.tsx @@ -5,12 +5,14 @@ * 2.0. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; export const UPGRADE_CONFLICTS_MODAL_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.messageTitle', { - defaultMessage: 'Update rules without conflicts?', + defaultMessage: 'There are rules with conflicts', } ); @@ -21,17 +23,135 @@ export const UPGRADE_CONFLICTS_MODAL_CANCEL = i18n.translate( } ); -export const UPGRADE_CONFLICTS_MODAL_CONFIRM = i18n.translate( - 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.confirmTitle', +export const UPGRADE_RULES_WITHOUT_CONFLICTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.upgradeRulesWithoutConflicts', { defaultMessage: 'Update rules without conflicts', } ); -export const UPGRADE_CONFLICTS_MODAL_BODY = i18n.translate( - 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.affectedJobsTitle', +export const UPGRADE_RULES_WITH_CONFLICTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeConflictsModal.upgradeRulesWithConflicts', { - defaultMessage: - "Some of the selected rules have conflicts and, for that reason, won't be updated. Resolve the conflicts to properly update the rules.", + defaultMessage: 'Update rules', } ); + +export const ONLY_RULES_WITH_SOLVABLE_CONFLICTS = (numOfRules: number) => ( + {numOfRules} }} + /> +); + +export const ONLY_RULES_WITH_NON_SOLVABLE_CONFLICTS = (numOfRules: number) => ( + {numOfRules} }} + /> +); + +export const ONLY_RULES_WITH_CONFLICTS = ({ + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, +}: { + numOfRulesWithSolvableConflicts: number; + numOfRulesWithNonSolvableConflicts: number; +}) => ( + {numOfRulesWithSolvableConflicts + numOfRulesWithNonSolvableConflicts} + ), + numOfRulesWithSolvableConflicts, + numOfRulesWithSolvableConflictsStrong: {numOfRulesWithSolvableConflicts}, + }} + /> +); + +export const RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_NON_SOLVABLE_CONFLICTS = ({ + numOfRulesWithoutConflicts, + numOfRulesWithNonSolvableConflicts, +}: { + numOfRulesWithoutConflicts: number; + numOfRulesWithNonSolvableConflicts: number; +}) => ( + {numOfRulesWithoutConflicts + numOfRulesWithNonSolvableConflicts} + ), + numOfRulesWithoutConflicts, + numOfRulesWithoutConflictsStrong: {numOfRulesWithoutConflicts}, + numOfRulesWithNonSolvableConflicts: {numOfRulesWithNonSolvableConflicts}, + }} + /> +); + +export const RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_SOLVABLE_CONFLICTS = ({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, +}: { + numOfRulesWithoutConflicts: number; + numOfRulesWithSolvableConflicts: number; +}) => ( + {numOfRulesWithoutConflicts + numOfRulesWithSolvableConflicts} + ), + numOfRulesWithoutConflicts, + numOfRulesWithoutConflictsStrong: {numOfRulesWithoutConflicts}, + numOfRulesWithSolvableConflictsStrong: {numOfRulesWithSolvableConflicts}, + }} + /> +); + +export const ALL_KINDS_OF_RULES = ({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, +}: { + numOfRulesWithoutConflicts: number; + numOfRulesWithSolvableConflicts: number; + numOfRulesWithNonSolvableConflicts: number; +}) => ( + <> + + {numOfRulesWithoutConflicts + + numOfRulesWithSolvableConflicts + + numOfRulesWithNonSolvableConflicts} + + ), + numOfRulesWithConflictsStrong: ( + {numOfRulesWithSolvableConflicts + numOfRulesWithNonSolvableConflicts} + ), + numOfRulesWithoutConflicts, + numOfRulesWithoutConflictsStrong: {numOfRulesWithoutConflicts}, + numOfRulesWithSolvableConflicts, + numOfRulesWithSolvableConflictsStrong: {numOfRulesWithSolvableConflicts}, + }} + /> + {numOfRulesWithNonSolvableConflicts > 0 && ( + + )} + +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/upgrade_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/upgrade_modal.tsx index 0b0c7bb9cc72c..bef509dda5013 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/upgrade_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/upgrade_modal.tsx @@ -5,31 +5,122 @@ * 2.0. */ -import { EuiConfirmModal, EuiText } from '@elastic/eui'; -import React, { memo } from 'react'; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; +import { ConfirmRulesUpgrade } from './use_upgrade_modal'; import * as i18n from './translations'; -interface UpgradeWithConflictsModalProps { +export interface RulesConflictStats { + numOfRulesWithoutConflicts: number; + numOfRulesWithSolvableConflicts: number; + numOfRulesWithNonSolvableConflicts: number; +} + +interface UpgradeWithConflictsModalProps extends RulesConflictStats { onCancel: () => void; - onConfirm: () => void; + onConfirm: (result: ConfirmRulesUpgrade) => void; } export const UpgradeWithConflictsModal = memo(function ConfirmUpgradeWithConflictsModal({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, onCancel, onConfirm, }: UpgradeWithConflictsModalProps): JSX.Element { + const confirmUpgradingRulesWithoutConflicts = useCallback( + () => onConfirm(ConfirmRulesUpgrade.WithoutConflicts), + [onConfirm] + ); + const confirmUpgradingRulesWithSolvableConflicts = useCallback( + () => onConfirm(ConfirmRulesUpgrade.WithSolvableConflicts), + [onConfirm] + ); + return ( - - {i18n.UPGRADE_CONFLICTS_MODAL_BODY} - + + + {i18n.UPGRADE_CONFLICTS_MODAL_TITLE} + + + + + {getModalBodyText({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, + })} + + + + + {numOfRulesWithoutConflicts > 0 && ( + + {i18n.UPGRADE_RULES_WITHOUT_CONFLICTS} + + )} + {numOfRulesWithSolvableConflicts > 0 && ( + + {i18n.UPGRADE_RULES_WITH_CONFLICTS} + + )} + {i18n.UPGRADE_CONFLICTS_MODAL_CANCEL} + + ); }); + +function getModalBodyText({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, +}: RulesConflictStats): JSX.Element { + // Only solvable conflicts + if (numOfRulesWithoutConflicts === 0 && numOfRulesWithNonSolvableConflicts === 0) { + return i18n.ONLY_RULES_WITH_SOLVABLE_CONFLICTS(numOfRulesWithSolvableConflicts); + } + + // Only non-solvable conflicts + if (numOfRulesWithoutConflicts === 0 && numOfRulesWithSolvableConflicts === 0) { + return i18n.ONLY_RULES_WITH_NON_SOLVABLE_CONFLICTS(numOfRulesWithNonSolvableConflicts); + } + + // Only conflicts + if (numOfRulesWithoutConflicts === 0) { + return i18n.ONLY_RULES_WITH_CONFLICTS({ + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, + }); + } + + // Rules without conflicts + rules with solvable conflicts + if (numOfRulesWithNonSolvableConflicts === 0) { + return i18n.RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_SOLVABLE_CONFLICTS({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, + }); + } + + // Rules without conflicts + rules with non-solvable conflicts + if (numOfRulesWithSolvableConflicts === 0) { + return i18n.RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_NON_SOLVABLE_CONFLICTS({ + numOfRulesWithoutConflicts, + numOfRulesWithNonSolvableConflicts, + }); + } + + return i18n.ALL_KINDS_OF_RULES({ + numOfRulesWithoutConflicts, + numOfRulesWithSolvableConflicts, + numOfRulesWithNonSolvableConflicts, + }); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/use_upgrade_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/use_upgrade_modal.tsx index 0714ba458d34e..4fbb7879471c7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/use_upgrade_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/use_upgrade_modal.tsx @@ -6,25 +6,53 @@ */ import type { ReactNode } from 'react'; -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { useBoolean } from '@kbn/react-hooks'; import { useAsyncConfirmation } from '../../rules_table/use_async_confirmation'; +import type { RulesConflictStats } from './upgrade_modal'; import { UpgradeWithConflictsModal } from './upgrade_modal'; +export enum ConfirmRulesUpgrade { + WithoutConflicts = 'WithoutConflicts', + WithSolvableConflicts = 'WithSolvableConflicts', +} + interface UseUpgradeWithConflictsModalResult { modal: ReactNode; - confirmConflictsUpgrade: () => Promise; + confirmConflictsUpgrade: ( + conflictsStats: RulesConflictStats + ) => Promise; } export function useUpgradeWithConflictsModal(): UseUpgradeWithConflictsModalResult { const [isVisible, { on: showModal, off: hideModal }] = useBoolean(false); - const [confirmConflictsUpgrade, confirm, cancel] = useAsyncConfirmation({ + const [initConfirmation, confirm, cancel] = useAsyncConfirmation({ onInit: showModal, onFinish: hideModal, }); + const [rulesUpgradeConflictsStats, setRulesUpgradeConflictsStats] = useState({ + numOfRulesWithoutConflicts: 0, + numOfRulesWithSolvableConflicts: 0, + numOfRulesWithNonSolvableConflicts: 0, + }); + + const confirmConflictsUpgrade = useCallback( + (conflictsStats: RulesConflictStats) => { + setRulesUpgradeConflictsStats(conflictsStats); + + return initConfirmation(); + }, + [initConfirmation] + ); return { - modal: isVisible && , + modal: isVisible && ( + + ), confirmConflictsUpgrade, }; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 33896a138aaa2..90d4ece58c23c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -702,6 +702,13 @@ export const COLUMN_MODIFIED = i18n.translate( } ); +export const COLUMN_CONFLICT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.conflictTitle', + { + defaultMessage: 'Conflict', + } +); + export const COLUMN_ENABLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.columns.enabledTitle', { @@ -863,6 +870,36 @@ export const RULE_EXECUTION_STATUS_FILTER = i18n.translate( } ); +export const SOLVABLE_CONFLICT_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeRules.solvableConflictLabel', + { + defaultMessage: 'Auto-resolved conflict', + } +); + +export const SOLVABLE_CONFLICT_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeRules.solvableConflictTooltipDescription', + { + defaultMessage: + 'This Elastic rule has auto-resolved conflicts that require review before upgrade.', + } +); + +export const NON_SOLVABLE_CONFLICT_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeRules.nonSolvableConflictLabel', + { + defaultMessage: 'Unresolved conflict', + } +); + +export const NON_SOLVABLE_CONFLICT_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeRules.nonSolvableConflictTooltipDescription', + { + defaultMessage: + 'This Elastic rule has unresolved conflicts that require editing before upgrade.', + } +); + export const NO_RULES = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesTitle', { @@ -1451,14 +1488,14 @@ export const INSTALL_RULE_BUTTON = i18n.translate( export const UPDATE_RULE_BUTTON = i18n.translate( 'xpack.securitySolution.addRules.upgradeRuleButton', { - defaultMessage: 'Update rule', + defaultMessage: 'Update', } ); export const REVIEW_RULE_BUTTON = i18n.translate( 'xpack.securitySolution.addRules.reviewRuleButton', { - defaultMessage: 'Review rule', + defaultMessage: 'Review', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts index 453ef862e4588..f8578411449b6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/create_upgradeable_rules_payload.ts @@ -14,6 +14,8 @@ import { type AllFieldsDiff, MissingVersion, } from '../../../../../../common/api/detection_engine'; +import type { UpgradeConflictResolution } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { UpgradeConflictResolutionEnum } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { assertPickVersionIsTarget } from './assert_pick_version_is_target'; @@ -40,7 +42,11 @@ export const createModifiedPrebuiltRuleAssets = ({ defaultPickVersion, }: CreateModifiedPrebuiltRuleAssetsProps) => { return withSecuritySpanSync(createModifiedPrebuiltRuleAssets.name, () => { - const { pick_version: globalPickVersion = defaultPickVersion, mode } = requestBody; + const { + pick_version: globalPickVersion = defaultPickVersion, + mode, + on_conflict: onConflict, + } = requestBody; const { modifiedPrebuiltRuleAssets, processingErrors } = upgradeableRules.reduce( @@ -77,7 +83,9 @@ export const createModifiedPrebuiltRuleAssets = ({ ) as AllFieldsDiff; if (mode === 'ALL_RULES' && globalPickVersion === 'MERGED') { - const fieldsWithConflicts = Object.keys(getFieldsDiffConflicts(calculatedRuleDiff)); + const fieldsWithConflicts = Object.keys( + getFieldsDiffConflicts(calculatedRuleDiff, onConflict) + ); if (fieldsWithConflicts.length > 0) { // If the mode is ALL_RULES, no fields can be overriden to any other pick_version // than "MERGED", so throw an error for the fields that have conflicts. @@ -152,7 +160,12 @@ function createModifiedPrebuiltRuleAsset({ return modifiedPrebuiltRuleAsset as PrebuiltRuleAsset; } -const getFieldsDiffConflicts = (ruleFieldsDiff: Partial) => - pickBy(ruleFieldsDiff, (diff) => { - return diff.conflict !== 'NONE'; - }); +const getFieldsDiffConflicts = ( + ruleFieldsDiff: Partial, + onConflict?: UpgradeConflictResolution +) => + pickBy(ruleFieldsDiff, (diff) => + onConflict === UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE + ? diff.conflict !== 'NONE' && diff.conflict !== 'SOLVABLE' + : diff.conflict !== 'NONE' + ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_for_field.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_for_field.ts index 00de04c291aeb..ffafbf916ad24 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_for_field.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_for_field.ts @@ -47,6 +47,7 @@ export const getValueForField = ({ pick_version: globalPickVersion, }, ruleFieldsDiff, + onConflict: requestBody.on_conflict, }) : getValueFromRuleTriad({ fieldName, @@ -85,6 +86,7 @@ export const getValueForField = ({ upgradeableRule, fieldUpgradeSpecifier, ruleFieldsDiff, + onConflict: requestBody.on_conflict, }) : getValueFromRuleTriad({ fieldName, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_from_rule_version.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_from_rule_version.ts index 3bef2ea7c742c..8d82d74f50f90 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_from_rule_version.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_value_from_rule_version.ts @@ -5,9 +5,11 @@ * 2.0. */ -import type { - RuleFieldsToUpgrade, - AllFieldsDiff, +import { + type RuleFieldsToUpgrade, + type AllFieldsDiff, + type UpgradeConflictResolution, + UpgradeConflictResolutionEnum, } from '../../../../../../common/api/detection_engine'; import { RULE_DEFAULTS } from '../../../rule_management/logic/detection_rules_client/mergers/apply_rule_defaults'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; @@ -24,11 +26,13 @@ export const getValueFromMergedVersion = ({ upgradeableRule, fieldUpgradeSpecifier, ruleFieldsDiff, + onConflict, }: { fieldName: keyof PrebuiltRuleAsset; upgradeableRule: RuleTriad; fieldUpgradeSpecifier: NonNullable; ruleFieldsDiff: AllFieldsDiff; + onConflict?: UpgradeConflictResolution; }) => { const ruleId = upgradeableRule.target.rule_id; const diffableRuleFieldName = mapRuleFieldToDiffableRuleField({ @@ -39,7 +43,11 @@ export const getValueFromMergedVersion = ({ if (fieldUpgradeSpecifier.pick_version === 'MERGED') { const ruleFieldDiff = ruleFieldsDiff[diffableRuleFieldName]; - if (ruleFieldDiff && ruleFieldDiff.conflict !== 'NONE') { + if ( + ruleFieldDiff && onConflict === UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE + ? ruleFieldDiff.conflict !== 'NONE' && ruleFieldDiff.conflict !== 'SOLVABLE' + : ruleFieldDiff.conflict !== 'NONE' + ) { throw new Error( `Automatic merge calculation for field '${diffableRuleFieldName}' in rule of rule_id ${ruleId} resulted in a conflict. Please resolve the conflict manually or choose another value for 'pick_version'.` ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts index 6bea2c71123da..3a9a88870f8f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts @@ -10,12 +10,15 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { PerformRuleUpgradeRequestBody, PerformRuleUpgradeResponseBody, + RuleUpgradeSpecifier, SkippedRuleUpgrade, + ThreeWayDiff, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { ModeEnum, PickVersionValuesEnum, SkipRuleUpgradeReasonEnum, + ThreeWayDiffConflict, UpgradeConflictResolutionEnum, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; @@ -35,6 +38,7 @@ import type { } from '../../../../../../common/api/detection_engine'; import type { PromisePoolError } from '../../../../../utils/promise_pool'; import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; +import type { RuleVersions } from '../../logic/diff/calculate_rule_diff'; import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; @@ -155,12 +159,18 @@ export const performRuleUpgradeHandler = async ( // Check there's no conflicts if (onConflict === UpgradeConflictResolutionEnum.SKIP) { - const ruleDiff = calculateRuleDiff(ruleVersions); - const hasConflict = ruleDiff.ruleDiff.num_fields_with_conflicts > 0; - if (hasConflict) { + const ruleUpgradeSpecifier = + request.body.mode === ModeEnum.SPECIFIC_RULES + ? request.body.rules.find((x) => x.rule_id === targetRule.rule_id) + : undefined; + + const conflict = getRuleUpgradeConflictState(ruleVersions, ruleUpgradeSpecifier); + + if (conflict !== ThreeWayDiffConflict.NONE) { skippedRules.push({ rule_id: targetRule.rule_id, reason: SkipRuleUpgradeReasonEnum.CONFLICT, + conflict, }); return; } @@ -233,3 +243,39 @@ export const performRuleUpgradeHandler = async ( }); } }; + +function getRuleUpgradeConflictState( + ruleVersions: RuleVersions, + ruleUpgradeSpecifier?: RuleUpgradeSpecifier +): ThreeWayDiffConflict { + const { ruleDiff } = calculateRuleDiff(ruleVersions); + + if (ruleDiff.num_fields_with_conflicts === 0) { + return ThreeWayDiffConflict.NONE; + } + + if (!ruleUpgradeSpecifier) { + return ruleDiff.num_fields_with_non_solvable_conflicts > 0 + ? ThreeWayDiffConflict.NON_SOLVABLE + : ThreeWayDiffConflict.SOLVABLE; + } + + let result = ThreeWayDiffConflict.NONE; + + // filter out resolved fields + for (const [fieldName, fieldThreeWayDiff] of Object.entries>( + ruleDiff.fields + )) { + const hasResolvedValue = + ruleUpgradeSpecifier.fields?.[fieldName as keyof typeof ruleUpgradeSpecifier.fields] + ?.pick_version === 'RESOLVED'; + + if (fieldThreeWayDiff.conflict === ThreeWayDiffConflict.NON_SOLVABLE && !hasResolvedValue) { + return ThreeWayDiffConflict.NON_SOLVABLE; + } else if (fieldThreeWayDiff.conflict === ThreeWayDiffConflict.SOLVABLE && !hasResolvedValue) { + result = ThreeWayDiffConflict.SOLVABLE; + } + } + + return result; +}