From e441962a67316faf459e5da45f5246fb80ae100e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:07:32 +1100 Subject: [PATCH] [9.0] [Security Solution] Add UI incentivizers to upgrade prebuilt rules (#211862) (#213234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `9.0`: - [[Security Solution] Add UI incentivizers to upgrade prebuilt rules (#211862)](https://github.com/elastic/kibana/pull/211862) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../diff/three_way_diff/three_way_diff.ts | 3 +- .../review_rule_upgrade_route.ts | 5 +- .../detection_engine/rule_management/utils.ts | 5 +- .../pages/rule_editing/index.tsx | 13 + .../pages/rule_details/index.tsx | 10 +- .../rule_details/rule_update_callout.tsx | 71 +++ .../missing_base_version_callout.tsx | 16 + .../rule_upgrade/rule_upgrade.tsx | 7 + .../rule_upgrade/rule_upgrade_callout.tsx | 98 ++-- .../rule_upgrade/translations.tsx | 8 + .../components/rule_details/translations.tsx | 14 + .../hooks/use_prebuilt_rules_upgrade.tsx | 423 ++++++++++++++++++ .../hooks/use_rule_update_callout.tsx | 32 ++ .../rule_upgrade_state.ts | 4 + .../components/mini_callout/translations.tsx | 2 +- .../rule_update_callouts.tsx | 14 +- .../add_prebuilt_rules_table.tsx | 2 +- .../feature_tour/rules_feature_tour.tsx | 2 +- .../upgrade_prebuilt_rules_table.tsx | 2 +- .../upgrade_prebuilt_rules_table_context.tsx | 395 ++-------------- .../use_prebuilt_rules_upgrade_state.test.ts | 1 + .../use_prebuilt_rules_upgrade_state.ts | 6 + ...e_upgrade_prebuilt_rules_table_columns.tsx | 42 +- .../pages/rule_management/index.tsx | 5 +- .../detection_engine/rules/translations.ts | 29 ++ .../create_upgradeable_rules_payload.ts | 28 +- .../calculate_rule_upgrade_info.ts | 2 + .../logic/diff/calculate_rule_diff.ts | 19 +- .../data_source_diff_algorithm.test.ts | 189 +++++--- .../algorithms/data_source_diff_algorithm.ts | 31 +- .../eql_query_diff_algorithm.test.ts | 174 ++++--- .../algorithms/eql_query_diff_algorithm.ts | 6 +- .../esql_query_diff_algorithm.test.ts | 160 ++++--- .../algorithms/esql_query_diff_algorithm.ts | 6 +- .../kql_query_diff_algorithm.test.ts | 370 ++++++++++----- .../algorithms/kql_query_diff_algorithm.ts | 5 +- .../multi_line_string_diff_algorithm.test.ts | 138 ++++-- .../multi_line_string_diff_algorithm.ts | 30 +- .../algorithms/number_diff_algorithm.test.ts | 132 ++++-- .../algorithms/number_diff_algorithm.ts | 5 +- .../rule_type_diff_algorithm.test.ts | 82 ++-- .../algorithms/rule_type_diff_algorithm.ts | 10 +- .../scalar_array_diff_algorithm.test.ts | 144 ++++-- .../algorithms/scalar_array_diff_algorithm.ts | 39 +- .../algorithms/simple_diff_algorithm.ts | 32 +- .../single_line_string_diff_algorithm.test.ts | 132 ++++-- .../single_line_string_diff_algorithm.ts | 5 +- .../calculation/calculate_rule_fields_diff.ts | 112 +++-- .../calculation/diff_calculation_helpers.ts | 5 +- .../common_fields/references.ts | 3 +- .../common_fields/tags.ts | 3 +- .../diffable_rule_fields/test_helpers.ts | 7 +- .../type_specific_fields/new_terms_fields.ts | 3 +- .../type_specific_fields/threat_index.ts | 3 +- .../update_workflow_customized_rules.cy.ts | 25 +- .../cypress/screens/alerts_detection_rules.ts | 4 + 56 files changed, 2051 insertions(+), 1062 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_update_callout.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts index c8c4238709849..c85d5e5824eae 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/three_way_diff/three_way_diff.ts @@ -145,5 +145,6 @@ export interface ThreeWayDiff { * Given the three versions of a value, calculates a three-way diff for it. */ export type ThreeWayDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ) => ThreeWayDiff; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts index 3fd95412b9311..40c92733851cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts @@ -10,7 +10,7 @@ import { SortOrder, type RuleObjectId, type RuleSignatureId, type RuleTagArray } import type { PartialRuleDiff } from '../model'; import type { RuleResponse, RuleVersion } from '../../model/rule_schema'; import { FindRulesSortField } from '../../rule_management'; -import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter'; +import { ReviewPrebuiltRuleUpgradeFilter } from '../common/review_prebuilt_rules_upgrade_filter'; export type ReviewRuleUpgradeSort = z.infer; export const ReviewRuleUpgradeSort = z.object({ @@ -27,7 +27,7 @@ export const ReviewRuleUpgradeSort = z.object({ export type ReviewRuleUpgradeRequestBody = z.infer; export const ReviewRuleUpgradeRequestBody = z .object({ - filter: PrebuiltRulesFilter.optional(), + filter: ReviewPrebuiltRuleUpgradeFilter.optional(), sort: ReviewRuleUpgradeSort.optional(), page: z.coerce.number().int().min(1).optional().default(1), @@ -89,4 +89,5 @@ export interface RuleUpgradeInfoForReview { target_rule: RuleResponse; diff: PartialRuleDiff; revision: number; + has_base_version: boolean; } diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts index c8e32c471c2c5..84f095463ef74 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/utils.ts @@ -6,7 +6,7 @@ */ import { ecsFieldMap } from '@kbn/alerts-as-data-utils'; -import type { RequiredField, RequiredFieldInput } from '../../api/detection_engine'; +import type { RequiredField, RequiredFieldInput, RuleResponse } from '../../api/detection_engine'; /* Computes the boolean "ecs" property value for each required field based on the ECS field map. @@ -23,3 +23,6 @@ export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): R ecs: isEcsField, }; }); + +export const isRuleCustomized = (rule: RuleResponse) => + rule.rule_source.type === 'external' && rule.rule_source.is_customized === true; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 6643ad2328168..e64d51dad9d95 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -11,6 +11,7 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiResizableContainer, EuiSpacer, EuiTab, @@ -75,6 +76,7 @@ import { usePrebuiltRulesCustomizationStatus } from '../../../rule_management/lo import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../../rule_creation/components/alert_suppression_edit'; import { usePrebuiltRuleCustomizationUpsellingMessage } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rule_customization_upselling_message'; +import { useRuleUpdateCallout } from '../../../rule_management/hooks/use_rule_update_callout'; const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { const { addSuccess } = useAppToasts(); @@ -509,6 +511,16 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { [navigateToApp, ruleId] ); + const upgradeCallout = useRuleUpdateCallout({ + rule, + message: ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_MESSAGE, + actionButton: ( + + {ruleI18n.HAS_RULE_UPDATE_EDITING_CALLOUT_BUTTON} + + ), + }); + if ( redirectToDetections( isSignalIndexExists, @@ -550,6 +562,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { setIsRulePreviewVisible={setIsRulePreviewVisible} togglePanel={togglePanel} /> + {upgradeCallout} {invalidSteps.length > 0 && ( = ({ const { pollForSignalIndex } = useSignalHelpers(); const [rule, setRule] = useState(null); - const isLoading = ruleLoading && rule == null; + const isLoading = useMemo(() => ruleLoading && rule == null, [rule, ruleLoading]); const { starting: isStartingJobs, startMlJobs } = useStartMlJobs(); const startMlJobsIfNeeded = useCallback(async () => { @@ -394,6 +395,12 @@ const RuleDetailsPageComponent: React.FC = ({ const lastExecutionDate = lastExecution?.date ?? ''; const lastExecutionMessage = lastExecution?.message ?? ''; + const upgradeCallout = useRuleUpdateCallout({ + rule, + message: ruleI18n.HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE, + onUpgrade: refreshRule, + }); + const ruleStatusInfo = useMemo(() => { return ( <> @@ -556,6 +563,7 @@ const RuleDetailsPageComponent: React.FC = ({ <> + {upgradeCallout} {isBulkDuplicateConfirmationVisible && ( void; +} + +const RuleUpdateCalloutComponent = ({ + rule, + message, + actionButton, + onUpgrade, +}: RuleUpdateCalloutProps): JSX.Element | null => { + const { upgradeReviewResponse, rulePreviewFlyout, openRulePreview } = usePrebuiltRulesUpgrade({ + pagination: { + page: 1, // we only want to fetch one result + perPage: 1, + }, + filter: { rule_ids: [rule.id] }, + onUpgrade, + }); + + const isRuleUpgradeable = useMemo( + () => upgradeReviewResponse !== undefined && upgradeReviewResponse.total > 0, + [upgradeReviewResponse] + ); + + const updateCallToActionButton = useMemo(() => { + if (actionButton) { + return actionButton; + } + return ( + openRulePreview(rule.rule_id)} + data-test-subj="ruleDetailsUpdateRuleCalloutButton" + > + {i18n.HAS_RULE_UPDATE_CALLOUT_BUTTON} + + ); + }, [actionButton, openRulePreview, rule.rule_id]); + + if (!isRuleUpgradeable) { + return null; + } + + return ( + <> + +

{message}

+ {updateCallToActionButton} +
+ + {rulePreviewFlyout} + + ); +}; + +export const RuleUpdateCallout = React.memo(RuleUpdateCalloutComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx new file mode 100644 index 0000000000000..d32007b7aa820 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/missing_base_version_callout.tsx @@ -0,0 +1,16 @@ +/* + * 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 React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import * as i18n from './translations'; + +export const RuleHasMissingBaseVersionCallout = () => ( + +

{i18n.RULE_BASE_VERSION_IS_MISSING_DESCRIPTION}

+
+); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx index 1ef063ecb0df2..1ff08ee9b3f74 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/rule_upgrade.tsx @@ -18,6 +18,7 @@ import { RuleUpgradeInfoBar } from './rule_upgrade_info_bar'; import { RuleUpgradeCallout } from './rule_upgrade_callout'; import { FieldUpgrade } from './field_upgrade'; import { FieldUpgradeContextProvider } from './field_upgrade_context'; +import { RuleHasMissingBaseVersionCallout } from './missing_base_version_callout'; interface RuleUpgradeProps { ruleUpgradeState: RuleUpgradeState; @@ -45,6 +46,12 @@ export const RuleUpgrade = memo(function RuleUpgrade({ targetVersionNumber={ruleUpgradeState.target_rule.version} /> + {!ruleUpgradeState.has_base_version && ( + <> + + + + )} 0) { return ( - - {i18n.UPGRADE_STATUS} -   - -   - {i18n.RULE_HAS_CONFLICTS(numOfNonSolvableConflicts + numOfSolvableConflicts)} - - } - color="danger" - size="s" - > - {i18n.RULE_HAS_HARD_CONFLICTS_DESCRIPTION} -
    -
  • {i18n.RULE_HAS_HARD_CONFLICTS_KEEP_YOUR_CHANGES}
  • -
  • {i18n.RULE_HAS_HARD_CONFLICTS_ACCEPT_ELASTIC_UPDATE}
  • -
  • {i18n.RULE_HAS_HARD_CONFLICTS_EDIT_FINAL_VERSION}
  • -
-
+ <> + + {i18n.UPGRADE_STATUS} +   + +   + {i18n.RULE_HAS_CONFLICTS(numOfNonSolvableConflicts + numOfSolvableConflicts)} + + } + color="danger" + size="s" + > + {i18n.RULE_HAS_HARD_CONFLICTS_DESCRIPTION} +
    +
  • {i18n.RULE_HAS_HARD_CONFLICTS_KEEP_YOUR_CHANGES}
  • +
  • {i18n.RULE_HAS_HARD_CONFLICTS_ACCEPT_ELASTIC_UPDATE}
  • +
  • {i18n.RULE_HAS_HARD_CONFLICTS_EDIT_FINAL_VERSION}
  • +
+
+ ); } if (numOfSolvableConflicts > 0) { return ( + <> + + {i18n.UPGRADE_STATUS} +   + +   + {i18n.RULE_HAS_CONFLICTS(numOfSolvableConflicts)} + + } + color="warning" + size="s" + > + {i18n.RULE_HAS_SOFT_CONFLICTS_DESCRIPTION} +
    +
  • {i18n.RULE_HAS_SOFT_CONFLICTS_ACCEPT_SUGGESTED_UPDATE}
  • +
  • {i18n.RULE_HAS_SOFT_CONFLICTS_EDIT_FINAL_VERSION}
  • +
+
+ + ); + } + + return ( + <> {i18n.UPGRADE_STATUS}   - -   - {i18n.RULE_HAS_CONFLICTS(numOfSolvableConflicts)} + } - color="warning" + color="success" size="s" > - {i18n.RULE_HAS_SOFT_CONFLICTS_DESCRIPTION} -
    -
  • {i18n.RULE_HAS_SOFT_CONFLICTS_ACCEPT_SUGGESTED_UPDATE}
  • -
  • {i18n.RULE_HAS_SOFT_CONFLICTS_EDIT_FINAL_VERSION}
  • -
+

{i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}

- ); - } - - return ( - - {i18n.UPGRADE_STATUS} -   - - - } - color="success" - size="s" - > -

{i18n.RULE_IS_READY_FOR_UPGRADE_DESCRIPTION}

-
+ ); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx index 812ec8c808730..1c42cb99836fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/translations.tsx @@ -158,3 +158,11 @@ export const FIELD_MODIFIED_BADGE_DESCRIPTION = i18n.translate( 'This field value differs from the one provided in the original version of the rule.', } ); + +export const RULE_BASE_VERSION_IS_MISSING_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.upgradeFlyout.baseVersionMissingDescription', + { + defaultMessage: + "The original, unedited version of this Elastic rule couldn't be found. This sometimes happens when a rule hasn't been updated in a while. You can still update this rule, but will only have access to its current version and the incoming Elastic update. Updating Elastic rules more often can help you avoid this in the future. We encourage you to review this update carefully and ensure your changes are not accidentally overwritten.", + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx index b72ec31ac63b3..04efe7de06c43 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx @@ -461,3 +461,17 @@ export const LUCENE_LANGUAGE_LABEL = i18n.translate( defaultMessage: 'Lucene', } ); + +export const HAS_RULE_UPDATE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.updateCalloutTitle', + { + defaultMessage: 'Elastic rule update available', + } +); + +export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetailsUpdate.calloutButton', + { + defaultMessage: 'Review update', + } +); 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 new file mode 100644 index 0000000000000..c7a292d580908 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_prebuilt_rules_upgrade.tsx @@ -0,0 +1,423 @@ +/* + * 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 React, { useCallback, useMemo, useState } from 'react'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; +import type { ReviewPrebuiltRuleUpgradeFilter } from '../../../../common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter'; +import { FieldUpgradeStateEnum, type RuleUpgradeState } from '../model/prebuilt_rule_upgrade'; +import { PerFieldRuleDiffTab } from '../components/rule_details/per_field_rule_diff_tab'; +import { PrebuiltRulesCustomizationDisabledReason } from '../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import { useIsUpgradingSecurityPackages } from '../logic/use_upgrade_security_packages'; +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, +} 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 * 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'; +import { CustomizationDisabledCallout } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/customization_disabled_callout'; +import { RuleUpgradeTab } from '../components/rule_details/three_way_diff'; +import { TabContentPadding } from '../../../siem_migrations/rules/components/rule_details_flyout'; +import { RuleTypeChangeCallout } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/rule_type_change_callout'; +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'; + +const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; + +export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; + +export interface UsePrebuiltRulesUpgradeParams { + pagination?: { + page: number; + perPage: number; + }; + sort?: { order: UpgradePrebuiltRulesSortingOptions['order']; field: FindRulesSortField }; + filter: ReviewPrebuiltRuleUpgradeFilter; + onUpgrade?: () => void; +} + +export function usePrebuiltRulesUpgrade({ + pagination = { page: 1, perPage: RULES_TABLE_INITIAL_PAGE_SIZE }, + sort, + filter, + onUpgrade, +}: UsePrebuiltRulesUpgradeParams) { + const { isRulesCustomizationEnabled, customizationDisabledReason } = + usePrebuiltRulesCustomizationStatus(); + const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); + const [loadingRules, setLoadingRules] = useState([]); + + const { + data: upgradeReviewResponse, + refetch, + dataUpdatedAt, + isFetched, + isLoading, + isFetching, + isRefetching, + } = usePrebuiltRulesUpgradeReview( + { + page: pagination.page, + per_page: pagination.perPage, + sort, + filter, + }, + { + refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + } + ); + + const upgradeableRules = useMemo( + () => upgradeReviewResponse?.rules ?? [], + [upgradeReviewResponse] + ); + + const { rulesUpgradeState, setRuleFieldResolvedValue } = + usePrebuiltRulesUpgradeState(upgradeableRules); + const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]); + + const { + modal: confirmLegacyMlJobsUpgradeModal, + confirmLegacyMLJobs, + isLoading: areMlJobsLoading, + } = useOutdatedMlJobsUpgradeModal(); + const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); + + const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); + + 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) => ({ + rule_id: ruleId, + version: rulesUpgradeState[ruleId].target_rule.version, + revision: rulesUpgradeState[ruleId].revision, + fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]), + })); + + setLoadingRules((prev) => [...prev, ...upgradingRuleIds]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) { + return; + } + + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'MERGED', + rules: ruleUpgradeSpecifiers, + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + const upgradedRuleIdsSet = new Set(upgradingRuleIds); + + if (onUpgrade) { + onUpgrade(); + } + + setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); + } + }, + [ + rulesUpgradeState, + confirmLegacyMLJobs, + confirmConflictsUpgrade, + upgradeRulesRequest, + onUpgrade, + ] + ); + + const upgradeRulesToTarget = useCallback( + async (ruleIds: RuleSignatureId[]) => { + const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({ + rule_id: ruleId, + version: rulesUpgradeState[ruleId].target_rule.version, + revision: rulesUpgradeState[ruleId].revision, + })); + + setLoadingRules((prev) => [...prev, ...ruleIds]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'TARGET', + rules: ruleUpgradeSpecifiers, + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + const upgradedRuleIdsSet = new Set(ruleIds); + + if (onUpgrade) { + onUpgrade(); + } + + setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); + } + }, + [confirmLegacyMLJobs, onUpgrade, rulesUpgradeState, upgradeRulesRequest] + ); + + const upgradeRules = useCallback( + async (ruleIds: RuleSignatureId[]) => { + if (isRulesCustomizationEnabled) { + await upgradeRulesToResolved(ruleIds); + } else { + await upgradeRulesToTarget(ruleIds); + } + }, + [isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget] + ); + + const upgradeAllRules = useCallback(async () => { + setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + 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({ + 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 + } finally { + setLoadingRules([]); + } + }, [ + upgradeableRules, + confirmLegacyMLJobs, + upgradeRulesRequest, + isRulesCustomizationEnabled, + filter, + confirmConflictsUpgrade, + ]); + + const subHeaderFactory = useCallback( + (rule: RuleResponse) => + rulesUpgradeState[rule.rule_id] ? ( + + ) : null, + [rulesUpgradeState] + ); + const ruleActionsFactory = useCallback( + (rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => { + const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; + if (!ruleUpgradeState) { + return null; + } + + const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; + return ( + { + if (hasRuleTypeChange || isRulesCustomizationEnabled === false) { + // If there is a rule type change, we can't resolve conflicts, only accept the target rule + upgradeRulesToTarget([rule.rule_id]); + } else { + upgradeRulesToResolved([rule.rule_id]); + } + closeRulePreview(); + }} + fill + data-test-subj="updatePrebuiltRuleFromFlyoutButton" + > + {i18n.UPDATE_BUTTON_LABEL} + + ); + }, + [ + rulesUpgradeState, + loadingRules, + isRefetching, + isUpgradingSecurityPackages, + isRulesCustomizationEnabled, + upgradeRulesToTarget, + upgradeRulesToResolved, + ] + ); + const extraTabsFactory = useCallback( + (rule: RuleResponse) => { + const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; + + if (!ruleUpgradeState) { + return []; + } + + const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; + const hasCustomizations = + ruleUpgradeState.current_rule.rule_source.type === 'external' && + ruleUpgradeState.current_rule.rule_source.is_customized; + + let headerCallout = null; + if ( + hasCustomizations && + customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License + ) { + headerCallout = ; + } else if (hasRuleTypeChange && isRulesCustomizationEnabled) { + headerCallout = ; + } + + let updateTabContent = ( + + ); + + // Show the resolver tab only if rule customization is enabled and there + // is no rule type change. In case of rule type change users can't resolve + // conflicts, only accept the target rule. + if (isRulesCustomizationEnabled && !hasRuleTypeChange) { + updateTabContent = ( + + ); + } + + const updatesTab = { + id: 'updates', + name: ( + + <>{ruleDetailsI18n.UPDATES_TAB_LABEL} + + ), + content: {updateTabContent}, + }; + + const jsonViewTab = { + id: 'jsonViewUpdates', + name: ( + + <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} + + ), + content: ( +
+ +
+ ), + }; + + return [updatesTab, jsonViewTab]; + }, + [ + rulesUpgradeState, + customizationDisabledReason, + isRulesCustomizationEnabled, + setRuleFieldResolvedValue, + ] + ); + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), + subHeaderFactory, + ruleActionsFactory, + extraTabsFactory, + flyoutProps: { + id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + }, + }); + + return { + ruleUpgradeStates, + upgradeReviewResponse, + isFetched, + isLoading: isLoading || areMlJobsLoading, + isFetching, + isRefetching, + isUpgradingSecurityPackages, + loadingRules, + lastUpdated: dataUpdatedAt, + rulePreviewFlyout, + confirmLegacyMlJobsUpgradeModal, + upgradeConflictsModal, + openRulePreview, + reFetchRules: refetch, + upgradeRules, + upgradeAllRules, + }; +} + +function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade { + const ruleFieldsToUpgrade: Record = {}; + + for (const [fieldName, fieldUpgradeState] of Object.entries( + ruleUpgradeState.fieldsUpgradeState + )) { + if (fieldUpgradeState.state === FieldUpgradeStateEnum.Accepted) { + ruleFieldsToUpgrade[fieldName] = { + pick_version: 'RESOLVED', + resolved_value: fieldUpgradeState.resolvedValue, + }; + } + } + + return ruleFieldsToUpgrade; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx new file mode 100644 index 0000000000000..a4bff69d4fb99 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/hooks/use_rule_update_callout.tsx @@ -0,0 +1,32 @@ +/* + * 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 React from 'react'; +import type { RuleResponse } from '../../../../common/api/detection_engine'; +import { RuleUpdateCallout } from '../components/rule_details/rule_update_callout'; + +interface UseRuleUpdateCalloutProps { + rule: RuleResponse | null; + message: string; + actionButton?: JSX.Element; + onUpgrade?: () => void; +} + +export const useRuleUpdateCallout = ({ + rule, + message, + actionButton, + onUpgrade, +}: UseRuleUpdateCalloutProps): JSX.Element | null => + !rule || rule.rule_source.type !== 'external' ? null : ( + + ); 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 7cd1f9c9ef843..45428fa1bb387 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 @@ -17,4 +17,8 @@ export interface RuleUpgradeState extends RuleUpgradeInfoForReview { * Indicates whether there are conflicts blocking rule upgrading. */ hasUnresolvedConflicts: boolean; + /** + * Indicates whether there are non-solvable conflicts blocking rule upgrading. + */ + hasNonSolvableUnresolvedConflicts: boolean; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx index 4d6a26042696c..ff55d01fd4b56 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/mini_callout/translations.tsx @@ -34,7 +34,7 @@ type OnClick = () => void; export const getUpdateRulesCalloutTitle = (onClick: OnClick) => ( { +interface RuleUpdateCalloutsProps { + shouldShowNewRulesCallout?: boolean; + shouldShowUpdateRulesCallout?: boolean; +} + +export const RuleUpdateCallouts = ({ + shouldShowNewRulesCallout = false, + shouldShowUpdateRulesCallout = false, +}: RuleUpdateCalloutsProps) => { const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus(); const rulesToInstallCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0; const rulesToUpgradeCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0; // Check against rulesInstalledCount since we don't want to show banners if we're showing the empty prompt - const shouldDisplayNewRulesCallout = rulesToInstallCount > 0; - const shouldDisplayUpdateRulesCallout = rulesToUpgradeCount > 0; + const shouldDisplayNewRulesCallout = shouldShowNewRulesCallout && rulesToInstallCount > 0; + const shouldDisplayUpdateRulesCallout = shouldShowUpdateRulesCallout && rulesToUpgradeCount > 0; const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); const { href } = getSecuritySolutionLinkProps({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx index 1baf002926d68..db4a7bc39e6c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx @@ -78,7 +78,7 @@ export const AddPrebuiltRulesTable = React.memo(() => { ) : ( <> - + diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx index 96e05f490147c..28d798ff06a99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx @@ -26,8 +26,8 @@ import React, { useEffect, useMemo } from 'react'; import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constants'; import { useKibana } from '../../../../../common/lib/kibana'; import { useIsElementMounted } from '../rules_table/guided_onboarding/use_is_element_mounted'; -import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; import * as i18n from './translations'; +import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from '../../../../rule_management/hooks/use_prebuilt_rules_upgrade'; export interface RulesFeatureTourContextType { steps: EuiTourStepProps[]; 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.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.tsx index 959f20ef72c20..d17fddf37c058 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.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.tsx @@ -97,7 +97,7 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { ) : ( <> - + 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_context.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_context.tsx index b0c7f93234c99..c59f4ec4be57b 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_context.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_context.tsx @@ -5,46 +5,29 @@ * 2.0. */ -import { EuiButton, EuiToolTip } from '@elastic/eui'; import type { Dispatch, SetStateAction } from 'react'; -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; +import React, { createContext, useContext, useMemo, useState } from 'react'; import type { FindRulesSortField, PrebuiltRulesFilter, - RuleFieldsToUpgrade, - RuleUpgradeSpecifier, SortOrder, } from '../../../../../../common/api/detection_engine'; -import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { RuleUpgradeTab } from '../../../../rule_management/components/rule_details/three_way_diff'; -import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; -import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; -import type { - RuleResponse, - RuleSignatureId, -} from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; -import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; -import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; -import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; -import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; -import { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum'; -import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; -import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; -import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal'; -import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal'; -import { RuleTypeChangeCallout } from './rule_type_change_callout'; -import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader'; -import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; -import * as i18n from './translations'; -import { CustomizationDisabledCallout } from './customization_disabled_callout'; import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../constants'; import type { PaginationOptions } from '../../../../rule_management/logic'; import { usePrebuiltRulesStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; +import { usePrebuiltRulesUpgrade } from '../../../../rule_management/hooks/use_prebuilt_rules_upgrade'; -const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; +export interface UpgradePrebuiltRulesSortingOptions { + field: + | 'current_rule.name' + | 'current_rule.risk_score' + | 'current_rule.severity' + | 'current_rule.last_updated'; + order: SortOrder; +} export interface UpgradePrebuiltRulesSortingOptions { field: @@ -111,8 +94,6 @@ export interface UpgradePrebuiltRulesTableState { sortingOptions: UpgradePrebuiltRulesSortingOptions; } -export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; - export interface UpgradePrebuiltRulesTableActions { reFetchRules: () => void; upgradeRules: (ruleIds: RuleSignatureId[]) => void; @@ -146,9 +127,6 @@ interface UpgradePrebuiltRulesTableContextProviderProps { export const UpgradePrebuiltRulesTableContextProvider = ({ children, }: UpgradePrebuiltRulesTableContextProviderProps) => { - const { isRulesCustomizationEnabled, customizationDisabledReason } = - usePrebuiltRulesCustomizationStatus(); - // Use the data from the prebuilt rules status API to determine if there are // rules to upgrade because it returns information about all rules without filters const { data: prebuiltRulesStatusResponse } = usePrebuiltRulesStatus(); @@ -156,9 +134,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ (prebuiltRulesStatusResponse?.stats.num_prebuilt_rules_to_upgrade ?? 0) > 0; const tags = prebuiltRulesStatusResponse?.aggregated_fields?.upgradeable_rules.tags; - const [loadingRules, setLoadingRules] = useState([]); const [filterOptions, setFilterOptions] = useState({}); - const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); const [pagination, setPagination] = useState({ page: 1, perPage: RULES_TABLE_INITIAL_PAGE_SIZE, @@ -182,319 +158,34 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ ); const { - data: upgradeReviewResponse, - refetch, - dataUpdatedAt, + ruleUpgradeStates, + upgradeReviewResponse, isFetched, isLoading, isFetching, isRefetching, - } = usePrebuiltRulesUpgradeReview( - { - page: pagination.page, - per_page: pagination.perPage, - sort: { - field: findRulesSortField, - order: sortingOptions.order, - }, - filter: filterOptions, - }, - { - refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, - keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change - } - ); - - const upgradeableRules = useMemo( - () => upgradeReviewResponse?.rules ?? [], - [upgradeReviewResponse] - ); - - const { rulesUpgradeState, setRuleFieldResolvedValue } = - usePrebuiltRulesUpgradeState(upgradeableRules); - const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]); - - const { - modal: confirmLegacyMlJobsUpgradeModal, - confirmLegacyMLJobs, - isLoading: areMlJobsLoading, - } = useOutdatedMlJobsUpgradeModal(); - const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); - - const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); - - 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) => ({ - rule_id: ruleId, - version: rulesUpgradeState[ruleId].target_rule.version, - revision: rulesUpgradeState[ruleId].revision, - fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]), - })); - - setLoadingRules((prev) => [...prev, ...upgradingRuleIds]); - - try { - // Handle MLJobs modal - if (!(await confirmLegacyMLJobs())) { - return; - } - - if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) { - return; - } - - await upgradeRulesRequest({ - mode: 'SPECIFIC_RULES', - pick_version: 'MERGED', - rules: ruleUpgradeSpecifiers, - }); - } catch { - // Error is handled by the mutation's onError callback, so no need to do anything here - } finally { - const upgradedRuleIdsSet = new Set(upgradingRuleIds); - - setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); - } - }, - [confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeRulesRequest] - ); - - const upgradeRulesToTarget = useCallback( - async (ruleIds: RuleSignatureId[]) => { - const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({ - rule_id: ruleId, - version: rulesUpgradeState[ruleId].target_rule.version, - revision: rulesUpgradeState[ruleId].revision, - })); - - setLoadingRules((prev) => [...prev, ...ruleIds]); - - try { - // Handle MLJobs modal - if (!(await confirmLegacyMLJobs())) { - return; - } - - await upgradeRulesRequest({ - mode: 'SPECIFIC_RULES', - pick_version: 'TARGET', - rules: ruleUpgradeSpecifiers, - }); - } catch { - // Error is handled by the mutation's onError callback, so no need to do anything here - } finally { - const upgradedRuleIdsSet = new Set(ruleIds); - - setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); - } - }, - [confirmLegacyMLJobs, rulesUpgradeState, upgradeRulesRequest] - ); - - const upgradeRules = useCallback( - async (ruleIds: RuleSignatureId[]) => { - if (isRulesCustomizationEnabled) { - await upgradeRulesToResolved(ruleIds); - } else { - await upgradeRulesToTarget(ruleIds); - } - }, - [isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget] - ); - - const upgradeAllRules = useCallback(async () => { - setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]); - - try { - // Handle MLJobs modal - if (!(await confirmLegacyMLJobs())) { - return; - } - - const dryRunResults = await upgradeRulesRequest({ - mode: 'ALL_RULES', - pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', - filter: filterOptions, - dry_run: true, - on_conflict: 'SKIP', - }); - - const hasConflicts = dryRunResults.results.skipped.some( - (skippedRule) => skippedRule.reason === 'CONFLICT' - ); - - if (hasConflicts && !(await confirmConflictsUpgrade())) { - return; - } - - await upgradeRulesRequest({ - mode: 'ALL_RULES', - pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', - filter: filterOptions, - on_conflict: 'SKIP', - }); - } catch { - // Error is handled by the mutation's onError callback, so no need to do anything here - } finally { - setLoadingRules([]); - } - }, [ - upgradeableRules, - confirmLegacyMLJobs, - upgradeRulesRequest, - isRulesCustomizationEnabled, - filterOptions, - confirmConflictsUpgrade, - ]); - - const subHeaderFactory = useCallback( - (rule: RuleResponse) => - rulesUpgradeState[rule.rule_id] ? ( - - ) : null, - [rulesUpgradeState] - ); - const ruleActionsFactory = useCallback( - (rule: RuleResponse, closeRulePreview: () => void, isEditingRule: boolean) => { - const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; - if (!ruleUpgradeState) { - return null; - } - - const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; - return ( - { - if (hasRuleTypeChange || isRulesCustomizationEnabled === false) { - // If there is a rule type change, we can't resolve conflicts, only accept the target rule - upgradeRulesToTarget([rule.rule_id]); - } else { - upgradeRulesToResolved([rule.rule_id]); - } - closeRulePreview(); - }} - fill - data-test-subj="updatePrebuiltRuleFromFlyoutButton" - > - {i18n.UPDATE_BUTTON_LABEL} - - ); - }, - [ - rulesUpgradeState, - loadingRules, - isRefetching, - isUpgradingSecurityPackages, - isRulesCustomizationEnabled, - upgradeRulesToTarget, - upgradeRulesToResolved, - ] - ); - const extraTabsFactory = useCallback( - (rule: RuleResponse) => { - const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; - - if (!ruleUpgradeState) { - return []; - } - - const hasRuleTypeChange = ruleUpgradeState.diff.fields.type?.has_update ?? false; - const hasCustomizations = - ruleUpgradeState.current_rule.rule_source.type === 'external' && - ruleUpgradeState.current_rule.rule_source.is_customized; - - let headerCallout = null; - if ( - hasCustomizations && - customizationDisabledReason === PrebuiltRulesCustomizationDisabledReason.License - ) { - headerCallout = ; - } else if (hasRuleTypeChange && isRulesCustomizationEnabled) { - headerCallout = ; - } - - let updateTabContent = ( - - ); - - // Show the resolver tab only if rule customization is enabled and there - // is no rule type change. In case of rule type change users can't resolve - // conflicts, only accept the target rule. - if (isRulesCustomizationEnabled && !hasRuleTypeChange) { - updateTabContent = ( - - ); - } - - const updatesTab = { - id: 'updates', - name: ( - - <>{ruleDetailsI18n.UPDATES_TAB_LABEL} - - ), - content: {updateTabContent}, - }; - - const jsonViewTab = { - id: 'jsonViewUpdates', - name: ( - - <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} - - ), - content: ( -
- -
- ), - }; - - return [updatesTab, jsonViewTab]; - }, - [ - rulesUpgradeState, - customizationDisabledReason, - isRulesCustomizationEnabled, - setRuleFieldResolvedValue, - ] - ); - const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ - rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), - subHeaderFactory, - ruleActionsFactory, - extraTabsFactory, - flyoutProps: { - id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, - dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + isUpgradingSecurityPackages, + loadingRules, + lastUpdated, + rulePreviewFlyout, + confirmLegacyMlJobsUpgradeModal, + upgradeConflictsModal, + openRulePreview, + reFetchRules, + upgradeRules, + upgradeAllRules, + } = usePrebuiltRulesUpgrade({ + pagination, + sort: { + field: findRulesSortField, + order: sortingOptions.order, }, + filter: filterOptions, }); const actions = useMemo( () => ({ - reFetchRules: refetch, + reFetchRules, upgradeRules, upgradeAllRules, setFilterOptions, @@ -502,7 +193,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setPagination, setSortingOptions, }), - [refetch, upgradeRules, upgradeAllRules, openRulePreview] + [reFetchRules, upgradeRules, upgradeAllRules, openRulePreview] ); const providerValue = useMemo( @@ -513,12 +204,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ filterOptions, tags: tags ?? [], isFetched, - isLoading: isLoading || areMlJobsLoading, + isLoading, isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, - lastUpdated: dataUpdatedAt, + lastUpdated, pagination: { ...pagination, total: upgradeReviewResponse?.total ?? 0, @@ -534,12 +225,11 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ tags, isFetched, isLoading, - areMlJobsLoading, isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, - dataUpdatedAt, + lastUpdated, pagination, upgradeReviewResponse?.total, sortingOptions, @@ -568,20 +258,3 @@ export const useUpgradePrebuiltRulesTableContext = (): UpgradePrebuiltRulesConte return rulesTableContext; }; - -function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade { - const ruleFieldsToUpgrade: Record = {}; - - for (const [fieldName, fieldUpgradeState] of Object.entries( - ruleUpgradeState.fieldsUpgradeState - )) { - if (fieldUpgradeState.state === FieldUpgradeStateEnum.Accepted) { - ruleFieldsToUpgrade[fieldName] = { - pick_version: 'RESOLVED', - resolved_value: fieldUpgradeState.resolvedValue, - }; - } - } - - return ruleFieldsToUpgrade; -} 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.test.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.test.ts index 3a35e783975e9..f83aa914432a6 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.test.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.test.ts @@ -429,6 +429,7 @@ function createRuleUpgradeInfoMock( num_fields_with_non_solvable_conflicts: 0, fields: {}, }, + has_base_version: true, version: 1, revision: 1, ...rewrites, 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 706bda1eb0d66..2a20068ab8639 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 @@ -134,6 +134,9 @@ export function usePrebuiltRulesUpgradeState( fieldState === FieldUpgradeStateEnum.SolvableConflict || fieldState === FieldUpgradeStateEnum.NonSolvableConflict ); + const hasNonSolvableFieldConflicts = Object.values(fieldsUpgradeState).some( + ({ state: fieldState }) => fieldState === FieldUpgradeStateEnum.NonSolvableConflict + ); state[ruleUpgradeInfo.rule_id] = { ...ruleUpgradeInfo, @@ -141,6 +144,9 @@ export function usePrebuiltRulesUpgradeState( hasUnresolvedConflicts: isRulesCustomizationEnabled ? hasRuleTypeChange || hasFieldConflicts : false, + hasNonSolvableUnresolvedConflicts: isRulesCustomizationEnabled + ? hasRuleTypeChange || hasNonSolvableFieldConflicts + : false, }; } 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 f7f0493b822ce..014f107687c12 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 @@ -144,6 +144,7 @@ const MODIFIED_COLUMN: TableColumn = { const createUpgradeButtonColumn = ( upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'], + openRulePreview: UpgradePrebuiltRulesTableActions['openRulePreview'], loadingRules: RuleSignatureId[], isDisabled: boolean, isPrebuiltRulesCustomizationEnabled: boolean @@ -154,7 +155,7 @@ const createUpgradeButtonColumn = ( const isRuleUpgrading = loadingRules.includes(ruleId); const isDisabledByConflicts = isPrebuiltRulesCustomizationEnabled && record.hasUnresolvedConflicts; - const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || isDisabledByConflicts; + const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled; const spinner = ( ); - const tooltipContent = isDisabledByConflicts - ? i18n.UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS - : undefined; + if (isDisabledByConflicts) { + return ( + + openRulePreview(ruleId)} + data-test-subj={`reviewSinglePrebuiltRuleButton-${ruleId}`} + > + {isRuleUpgrading ? spinner : i18n.REVIEW_RULE_BUTTON} + + + ); + } 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%', @@ -189,7 +199,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); const { state: { loadingRules, isRefetching, isUpgradingSecurityPackages }, - actions: { upgradeRules }, + actions: { upgradeRules, openRulePreview }, } = useUpgradePrebuiltRulesTableContext(); const isDisabled = isRefetching || isUpgradingSecurityPackages; @@ -234,6 +244,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { ? [ createUpgradeButtonColumn( upgradeRules, + openRulePreview, loadingRules, isDisabled, isRulesCustomizationEnabled @@ -246,6 +257,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { showRelatedIntegrations, hasCRUDPermissions, upgradeRules, + openRulePreview, loadingRules, isDisabled, isRulesCustomizationEnabled, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 17c5368a4b1c5..e64ecb9173e3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiSpacer } from '@elastic/eui'; import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared'; import React, { useCallback } from 'react'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; @@ -35,6 +35,7 @@ import { AllRules } from '../../components/rules_table'; import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context'; import { useInvalidateFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview_query'; import { HeaderPage } from '../../../../common/components/header_page'; +import { RuleUpdateCallouts } from '../../components/rule_update_callouts/rule_update_callouts'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -168,6 +169,8 @@ const RulesPageComponent: React.FC = () => {
+ + >( ruleDiff.fields, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts index ed4f142d20596..a5fd84f4b097a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isRuleCustomized } from '../../../../../../common/detection_engine/rule_management/utils'; import type { DiffableRule, FullRuleDiff, @@ -66,9 +67,8 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = const { base, current, target } = args; invariant(current != null, 'current version is required'); - const diffableCurrentVersion = convertRuleToDiffable( - convertPrebuiltRuleAssetToRuleResponse(current) - ); + const diffableCurrentVersion = convertRuleToDiffable(current); + const isCustomized = isRuleCustomized(current); invariant(target != null, 'target version is required'); const diffableTargetVersion = convertRuleToDiffable( @@ -80,11 +80,14 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = ? convertRuleToDiffable(convertPrebuiltRuleAssetToRuleResponse(base)) : undefined; - const fieldsDiff = calculateRuleFieldsDiff({ - base_version: diffableBaseVersion || MissingVersion, - current_version: diffableCurrentVersion, - target_version: diffableTargetVersion, - }); + const fieldsDiff = calculateRuleFieldsDiff( + { + base_version: diffableBaseVersion || MissingVersion, + current_version: diffableCurrentVersion, + target_version: diffableTargetVersion, + }, + isCustomized + ); const { numberFieldsWithUpdates, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts index 6020738bd5e66..237c1a5651220 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.test.ts @@ -36,7 +36,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -55,7 +55,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '123' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -82,7 +82,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -110,7 +110,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -135,7 +135,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -159,7 +159,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -178,7 +178,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '456' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -208,7 +208,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -227,7 +227,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '456' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -262,7 +262,7 @@ describe('dataSourceDiffAlgorithm', () => { index_patterns: ['one', 'four', 'five'], }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -281,7 +281,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '789' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -311,7 +311,7 @@ describe('dataSourceDiffAlgorithm', () => { index_patterns: ['one', 'three', 'four', 'two', 'five'], }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -338,7 +338,7 @@ describe('dataSourceDiffAlgorithm', () => { data_view_id: '123', }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -360,7 +360,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '789' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -385,7 +385,7 @@ describe('dataSourceDiffAlgorithm', () => { target_version: { type: DataSourceType.data_view, data_view_id: '789' }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -407,7 +407,7 @@ describe('dataSourceDiffAlgorithm', () => { }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -421,81 +421,162 @@ describe('dataSourceDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: DataSourceType.index_patterns, - index_patterns: ['one', 'three', 'four'], - }, - target_version: { - type: DataSourceType.index_patterns, - index_patterns: ['one', 'three', 'four'], - }, - }; - - const result = dataSourceDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); - }); - - describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - it('if versions are different types', () => { + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, - current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, target_version: { type: DataSourceType.index_patterns, index_patterns: ['one', 'three', 'four'], }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ has_base_version: false, base_version: undefined, merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, + conflict: ThreeWayDiffConflict.NONE, }) ); }); - it('if current version is undefined', () => { + it('returns NONE conflict if rule is customized', () => { const mockVersions: ThreeVersionsOf = { base_version: MissingVersion, - current_version: undefined, + current_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, target_version: { type: DataSourceType.index_patterns, index_patterns: ['one', 'three', 'four'], }, }; - const result = dataSourceDiffAlgorithm(mockVersions); + const result = dataSourceDiffAlgorithm(mockVersions, true); expect(result).toEqual( expect.objectContaining({ has_base_version: false, base_version: undefined, merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, + conflict: ThreeWayDiffConflict.NONE, }) ); }); }); + + describe('if current_version and target_version are different - scenario -AB', () => { + describe('returns NONE conflict if rule is NOT customized', () => { + it('if versions are different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('if current version is undefined', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: undefined, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + }); + + describe('returns SOLVABLE conflict if rule is customized', () => { + it('if versions are different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { type: DataSourceType.data_view, data_view_id: '456' }, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); + + it('if current version is undefined', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: undefined, + target_version: { + type: DataSourceType.index_patterns, + index_patterns: ['one', 'three', 'four'], + }, + }; + + const result = dataSourceDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); + }); + }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts index 86aa886592468..368025d612dca 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/data_source_diff_algorithm.ts @@ -27,7 +27,8 @@ import { getDedupedDataSourceVersion, mergeDedupedArrays } from './helpers'; * Takes a type of `RuleDataSource | undefined` because the data source can be index patterns, a data view id, or neither in some cases */ export const dataSourceDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -46,6 +47,7 @@ export const dataSourceDiffAlgorithm = ( currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -73,6 +75,7 @@ interface MergeArgs { currentVersion: RuleDataSource | undefined; targetVersion: RuleDataSource | undefined; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ @@ -80,6 +83,7 @@ const mergeVersions = ({ currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { const dedupedBaseVersion = baseVersion ? getDedupedDataSourceVersion(baseVersion) : baseVersion; const dedupedCurrentVersion = currentVersion @@ -90,9 +94,6 @@ const mergeVersions = ({ : targetVersion; switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -140,14 +141,26 @@ const mergeVersions = ({ }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: dedupedTargetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + } + + // Missing base versions always return target version + // If the rule is customized, we return a SOLVABLE conflict + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { - mergedVersion: targetVersion, + conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, + mergedVersion: dedupedTargetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } default: diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts index acbb63016aeda..1b51b8a7e47ed 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.test.ts @@ -37,7 +37,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -68,7 +68,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -99,7 +99,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -130,7 +130,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -162,7 +162,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -193,7 +193,7 @@ describe('eqlQueryDiffAlgorithm', () => { }, }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -207,62 +207,124 @@ describe('eqlQueryDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'eql', - filters: [], - }, - target_version: { - query: 'query where true', - language: 'eql', - filters: [], - }, - }; + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + }; + + const result = eqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'eql', - filters: [], - }, - target_version: { - query: 'query where false', - language: 'eql', - filters: [], - }, - }; + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + }; - const result = eqlQueryDiffAlgorithm(mockVersions); + const result = eqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'eql', + filters: [], + }, + target_version: { + query: 'query where false', + language: 'eql', + filters: [], + }, + }; + + const result = eqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts index fa3e87397a2a7..c29e7d3358c92 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/eql_query_diff_algorithm.ts @@ -14,5 +14,7 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm'; /** * Diff algorithm for eql query types */ -export const eqlQueryDiffAlgorithm = (versions: ThreeVersionsOf) => - simpleDiffAlgorithm(versions); +export const eqlQueryDiffAlgorithm = ( + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts index ecaaa4142b5e0..96ab96d36a670 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.test.ts @@ -34,7 +34,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -62,7 +62,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -90,7 +90,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -118,7 +118,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -147,7 +147,7 @@ describe('esqlQueryDiffAlgorithm', () => { }, }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -161,58 +161,116 @@ describe('esqlQueryDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'esql', - }, - target_version: { - query: 'query where true', - language: 'esql', - }, - }; + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where true', + language: 'esql', + }, + }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where true', + language: 'esql', + }, + }; + + const result = esqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - query: 'query where true', - language: 'esql', - }, - target_version: { - query: 'query where false', - language: 'esql', - }, - }; + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where false', + language: 'esql', + }, + }; - const result = esqlQueryDiffAlgorithm(mockVersions); + const result = esqlQueryDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + query: 'query where true', + language: 'esql', + }, + target_version: { + query: 'query where false', + language: 'esql', + }, + }; + + const result = esqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts index 8360800ad4676..1aa0496041d67 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/esql_query_diff_algorithm.ts @@ -14,5 +14,7 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm'; /** * Diff algorithm for esql query types */ -export const esqlQueryDiffAlgorithm = (versions: ThreeVersionsOf) => - simpleDiffAlgorithm(versions); +export const esqlQueryDiffAlgorithm = ( + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts index fe97a222448df..b6acdf7450fce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.test.ts @@ -43,7 +43,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -71,7 +71,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -103,7 +103,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -137,7 +137,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -171,7 +171,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -205,7 +205,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -237,7 +237,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -271,7 +271,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -302,7 +302,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -338,7 +338,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -372,7 +372,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -406,7 +406,7 @@ describe('kqlQueryDiffAlgorithm', () => { }, }; - const result = kqlQueryDiffAlgorithm(mockVersions); + const result = kqlQueryDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -422,124 +422,250 @@ describe('kqlQueryDiffAlgorithm', () => { describe('if base_version is missing', () => { describe('if current_version and target_version are the same - scenario -AA', () => { - it('returns current_version as merged output if all versions are inline query types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.inline_query, - query: 'query string = true', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - target_version: { - type: KqlQueryType.inline_query, - query: 'query string = true', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if rule is NOT customized', () => { + it('returns current_version as merged output if all versions are inline query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns current_version as merged output if all versions are saved query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns current_version as merged output if all versions are saved query types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.saved_query, - saved_query_id: 'saved-query-id', - }, - target_version: { - type: KqlQueryType.saved_query, - saved_query_id: 'saved-query-id', - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if rule is customized', () => { + it('returns current_version as merged output if all versions are inline query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns current_version as merged output if all versions are saved query types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + target_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id', + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); }); describe('if current_version and target_version are different - scenario -AB', () => { - it('returns target_version as merged output if current and target versions have the same types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.inline_query, - query: 'query string = true', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - target_version: { - type: KqlQueryType.inline_query, - query: 'query string = false', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('if rule is NOT customized', () => { + it('returns NONE conflict if current and target versions have the same types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if current and target versions have different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id-2', + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current and target versions have different types', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: { - type: KqlQueryType.saved_query, - saved_query_id: 'saved-query-id-2', - }, - target_version: { - type: KqlQueryType.inline_query, - query: 'query string = false', - language: KqlQueryLanguageEnum.kuery, - filters: [], - }, - }; - - const result = kqlQueryDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('if rule is customized', () => { + it('returns SOLVABLE conflict if current and target versions have the same types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.inline_query, + query: 'query string = true', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); + + it('returns SOLVABLE conflict if current and target versions have different types', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: { + type: KqlQueryType.saved_query, + saved_query_id: 'saved-query-id-2', + }, + target_version: { + type: KqlQueryType.inline_query, + query: 'query string = false', + language: KqlQueryLanguageEnum.kuery, + filters: [], + }, + }; + + const result = kqlQueryDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts index 500c3211cc9c2..4f0a61b479d79 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/kql_query_diff_algorithm.ts @@ -15,5 +15,6 @@ import { simpleDiffAlgorithm } from './simple_diff_algorithm'; * Diff algorithm for all kql query types (`inline_query` and `saved_query`) */ export const kqlQueryDiffAlgorithm = ( - versions: ThreeVersionsOf -) => simpleDiffAlgorithm(versions); + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts index 4ecb828a77d4c..532c24cb1caa4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.test.ts @@ -32,7 +32,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_A, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -51,7 +51,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_A, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -70,7 +70,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_B, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -89,7 +89,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_B, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -109,7 +109,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: TEXT_M_C, }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -130,7 +130,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: 'My description.\nThis is a MODIFIED second line.', }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -151,7 +151,7 @@ describe('multiLineStringDiffAlgorithm', () => { target_version: 'My EXCELLENT description.\nThis is a second line.', }; - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -171,7 +171,7 @@ describe('multiLineStringDiffAlgorithm', () => { }; const startTime = performance.now(); - const result = multiLineStringDiffAlgorithm(mockVersions); + const result = multiLineStringDiffAlgorithm(mockVersions, false); const endTime = performance.now(); // If the regex merge in this function takes over 2 sec, this test fails @@ -192,46 +192,92 @@ describe('multiLineStringDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: TEXT_M_A, - target_version: TEXT_M_A, - }; - - const result = multiLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if target_version as merged output if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_A, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_A, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: TEXT_M_A, - target_version: TEXT_M_B, - }; - - const result = multiLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('returns NONE conflict if current_version and target_version are different and rule is not customized - scenario -AB', () => { + it('returns NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_B, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: TEXT_M_A, + target_version: TEXT_M_B, + }; + + const result = multiLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts index e09d8e110bff0..4cfe5b782fb44 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/multi_line_string_diff_algorithm.ts @@ -24,7 +24,8 @@ import { * Diff algorithm used for string fields that contain multiple lines */ export const multiLineStringDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -42,6 +43,7 @@ export const multiLineStringDiffAlgorithm = ( currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -69,6 +71,7 @@ interface MergeArgs { currentVersion: string; targetVersion: string; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ @@ -76,11 +79,9 @@ const mergeVersions = ({ currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -118,14 +119,27 @@ const mergeVersions = ({ }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + } + + // Missing base versions always return target version + // If the rule is customized, we return a SOLVABLE conflict + // Since multi-line string fields are mergeable, we would typically return a merged value + // as per https://github.com/elastic/kibana/pull/211862, but with no base version we cannot + // complete a full diff merge and so just return the target version case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { + conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, mergedVersion: targetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } default: diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts index 837fb85c3b2c3..d1fa31605244e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.test.ts @@ -22,7 +22,7 @@ describe('numberDiffAlgorithm', () => { target_version: 1, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -41,7 +41,7 @@ describe('numberDiffAlgorithm', () => { target_version: 1, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -60,7 +60,7 @@ describe('numberDiffAlgorithm', () => { target_version: 2, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -79,7 +79,7 @@ describe('numberDiffAlgorithm', () => { target_version: 2, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -98,7 +98,7 @@ describe('numberDiffAlgorithm', () => { target_version: 3, }; - const result = numberDiffAlgorithm(mockVersions); + const result = numberDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -111,46 +111,92 @@ describe('numberDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 1, - target_version: 1, - }; - - const result = numberDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 1, + }; + + const result = numberDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 1, + }; + + const result = numberDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 1, - target_version: 2, - }; - - const result = numberDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 2, + }; + + const result = numberDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 1, + target_version: 2, + }; + + const result = numberDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts index 30c32a475ecfb..7737f8d143e5d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/number_diff_algorithm.ts @@ -9,5 +9,6 @@ import type { ThreeVersionsOf } from '../../../../../../../../common/api/detecti import { simpleDiffAlgorithm } from './simple_diff_algorithm'; export const numberDiffAlgorithm = ( - versions: ThreeVersionsOf -) => simpleDiffAlgorithm(versions); + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts index accf133ac71b3..4c77d61536005 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.test.ts @@ -119,47 +119,51 @@ describe('ruleTypeDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'query', - target_version: 'query', - }; - - const result = ruleTypeDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - // User can change rule type field between `query` and `saved_query` in the UI, no other rule types - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'query', - target_version: 'saved_query', - }; - - const result = ruleTypeDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.NON_SOLVABLE, - }) - ); + describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { + it('returns NON_SOLVABLE conflict', () => { + // User can change rule type field between `query` and `saved_query` in the UI, no other rule types + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'query', + target_version: 'saved_query', + }; + + const result = ruleTypeDiffAlgorithm(mockVersions); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NON_SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts index 0701d1e46d251..efb115feb7419 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/rule_type_diff_algorithm.ts @@ -69,8 +69,9 @@ const mergeVersions = ({ diffOutcome, }: MergeArgs): MergeResult => { switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: return { @@ -83,8 +84,9 @@ const mergeVersions = ({ case ThreeWayDiffOutcome.StockValueCanUpdate: // NOTE: This scenario is currently inaccessible via normal UI or API workflows, but the logic is covered just in case case ThreeWayDiffOutcome.CustomizedValueCanUpdate: - // Scenario -AB is treated as scenario ABC: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // We return all -AB rule type fields as NON_SOLVABLE, whether or not the rule is customized + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { mergedVersion: targetVersion, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts index f041aa139bf41..f420d84c8473f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.test.ts @@ -22,7 +22,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -41,7 +41,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -60,7 +60,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'four', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -79,7 +79,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'four', 'three'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -99,7 +99,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['three', 'four', 'five', 'six']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -112,46 +112,94 @@ describe('scalarArrayDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: ['one', 'two', 'three'], - target_version: ['one', 'two', 'three'], - }; + describe('returns target_version as merged output if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'two', 'three'], + }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'two', 'three'], + }; + + const result = scalarArrayDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: ['one', 'two', 'three'], - target_version: ['one', 'four', 'three'], - }; + describe('if current_version and target_version are different - scenario -AB', () => { + it('returns target_version as merged output and NONE conflict if rule is not customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'four', 'three'], + }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns merged version of current and target as merged output if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: ['one', 'two', 'three'], + target_version: ['one', 'four', 'three'], + }; + + const expectedMergedVersion = ['one', 'two', 'three', 'four']; + + const result = scalarArrayDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: expectedMergedVersion, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Merged, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); @@ -163,7 +211,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['three', 'one', 'two'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -184,7 +232,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['one', 'two']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -204,7 +252,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['one', 'two']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -224,7 +272,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['one', 'two']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -244,7 +292,7 @@ describe('scalarArrayDiffAlgorithm', () => { }; const expectedMergedVersion = ['three']; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -265,7 +313,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -284,7 +332,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: ['one', 'two'], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -303,7 +351,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: [], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -322,7 +370,7 @@ describe('scalarArrayDiffAlgorithm', () => { target_version: [], }; - const result = scalarArrayDiffAlgorithm(mockVersions); + const result = scalarArrayDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts index 215e92377a596..92ceadc50576e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/scalar_array_diff_algorithm.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { uniq } from 'lodash'; +import { union, uniq } from 'lodash'; import { assertUnreachable } from '../../../../../../../../common/utility_types'; import type { ThreeVersionsOf, @@ -27,7 +27,8 @@ import { mergeDedupedArrays } from './helpers'; * NOTE: Diffing logic will be agnostic to array order */ export const scalarArrayDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -45,6 +46,7 @@ export const scalarArrayDiffAlgorithm = ( currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -72,6 +74,7 @@ interface MergeArgs { currentVersion: TValue[]; targetVersion: TValue[]; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ @@ -79,15 +82,13 @@ const mergeVersions = ({ currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { const dedupedBaseVersion = uniq(baseVersion); const dedupedCurrentVersion = uniq(currentVersion); const dedupedTargetVersion = uniq(targetVersion); switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -118,16 +119,34 @@ const mergeVersions = ({ mergeOutcome: ThreeWayMergeOutcome.Merged, }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { return { + conflict: ThreeWayDiffConflict.NONE, mergedVersion: targetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } + + // If the rule is customized, we return a SOLVABLE conflict with a merged outcome + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + return isRuleCustomized + ? { + mergedVersion: union(dedupedCurrentVersion, dedupedTargetVersion), + mergeOutcome: ThreeWayMergeOutcome.Merged, + conflict: ThreeWayDiffConflict.SOLVABLE, + } + : { + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }; + } default: return assertUnreachable(diffOutcome); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts index 1673b87003a00..f0b3ef9c66d9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/simple_diff_algorithm.ts @@ -25,7 +25,8 @@ import { * Meant to be used with primitive types (strings, numbers, booleans), NOT Arrays or Objects */ export const simpleDiffAlgorithm = ( - versions: ThreeVersionsOf + versions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreeWayDiff => { const { base_version: baseVersion, @@ -39,10 +40,10 @@ export const simpleDiffAlgorithm = ( const hasBaseVersion = baseVersion !== MissingVersion; const { mergeOutcome, conflict, mergedVersion } = mergeVersions({ - hasBaseVersion, currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }); return { @@ -66,22 +67,19 @@ interface MergeResult { } interface MergeArgs { - hasBaseVersion: boolean; currentVersion: TValue; targetVersion: TValue; diffOutcome: ThreeWayDiffOutcome; + isRuleCustomized: boolean; } const mergeVersions = ({ - hasBaseVersion, currentVersion, targetVersion, diffOutcome, + isRuleCustomized, }: MergeArgs): MergeResult => { switch (diffOutcome) { - // Scenario -AA is treated as scenario AAA: - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 - case ThreeWayDiffOutcome.MissingBaseNoUpdate: case ThreeWayDiffOutcome.StockValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueNoUpdate: case ThreeWayDiffOutcome.CustomizedValueSameUpdate: @@ -106,14 +104,26 @@ const mergeVersions = ({ }; } - // Scenario -AB is treated as scenario ABC, but marked as - // SOLVABLE, and returns the target version as the merged version - // https://github.com/elastic/kibana/pull/184889#discussion_r1636421293 + // Missing base versions always return target version + // Scenario -AA is treated as AAA + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 + case ThreeWayDiffOutcome.MissingBaseNoUpdate: { + return { + conflict: ThreeWayDiffConflict.NONE, + mergedVersion: targetVersion, + mergeOutcome: ThreeWayMergeOutcome.Target, + }; + } + + // Missing base versions always return target version + // If the rule is customized, we return a SOLVABLE conflict + // Otherwise we treat scenario -AB as AAB + // https://github.com/elastic/kibana/issues/210358#issuecomment-2654492854 case ThreeWayDiffOutcome.MissingBaseCanUpdate: { return { + conflict: isRuleCustomized ? ThreeWayDiffConflict.SOLVABLE : ThreeWayDiffConflict.NONE, mergedVersion: targetVersion, mergeOutcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, }; } default: diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts index af238283ca21f..cbeea00203e2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.test.ts @@ -22,7 +22,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'A', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -41,7 +41,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'A', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -60,7 +60,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'B', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -79,7 +79,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'B', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -98,7 +98,7 @@ describe('singleLineStringDiffAlgorithm', () => { target_version: 'C', }; - const result = singleLineStringDiffAlgorithm(mockVersions); + const result = singleLineStringDiffAlgorithm(mockVersions, false); expect(result).toEqual( expect.objectContaining({ @@ -111,46 +111,92 @@ describe('singleLineStringDiffAlgorithm', () => { }); describe('if base_version is missing', () => { - it('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'A', - target_version: 'A', - }; - - const result = singleLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.current_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, - merge_outcome: ThreeWayMergeOutcome.Current, - conflict: ThreeWayDiffConflict.NONE, - }) - ); + describe('returns current_version as merged output if current_version and target_version are the same - scenario -AA', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'A', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns NONE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'A', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseNoUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); }); - it('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { - const mockVersions: ThreeVersionsOf = { - base_version: MissingVersion, - current_version: 'A', - target_version: 'B', - }; - - const result = singleLineStringDiffAlgorithm(mockVersions); - - expect(result).toEqual( - expect.objectContaining({ - has_base_version: false, - base_version: undefined, - merged_version: mockVersions.target_version, - diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, - conflict: ThreeWayDiffConflict.SOLVABLE, - }) - ); + describe('returns target_version as merged output if current_version and target_version are different - scenario -AB', () => { + it('returns NONE conflict if rule is NOT customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'B', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, false); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.NONE, + }) + ); + }); + + it('returns SOLVABLE conflict if rule is customized', () => { + const mockVersions: ThreeVersionsOf = { + base_version: MissingVersion, + current_version: 'A', + target_version: 'B', + }; + + const result = singleLineStringDiffAlgorithm(mockVersions, true); + + expect(result).toEqual( + expect.objectContaining({ + has_base_version: false, + base_version: undefined, + merged_version: mockVersions.target_version, + diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + merge_outcome: ThreeWayMergeOutcome.Target, + conflict: ThreeWayDiffConflict.SOLVABLE, + }) + ); + }); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts index f80d8b63c8da8..f019e6298c850 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/algorithms/single_line_string_diff_algorithm.ts @@ -9,5 +9,6 @@ import type { ThreeVersionsOf } from '../../../../../../../../common/api/detecti import { simpleDiffAlgorithm } from './simple_diff_algorithm'; export const singleLineStringDiffAlgorithm = ( - versions: ThreeVersionsOf -) => simpleDiffAlgorithm(versions); + versions: ThreeVersionsOf, + isRuleCustomized: boolean +) => simpleDiffAlgorithm(versions, isRuleCustomized); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index b861a8432797b..77895e1b84484 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -60,9 +60,10 @@ const TARGET_TYPE_ERROR = `Target version can't be of different rule type`; * three-way diffs calculated for those fields. */ export const calculateRuleFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean = false ): RuleFieldsDiff => { - const commonFieldsDiff = calculateCommonFieldsDiff(ruleVersions); + const commonFieldsDiff = calculateCommonFieldsDiff(ruleVersions, isRuleCustomized); // eslint-disable-next-line @typescript-eslint/naming-convention const { base_version, current_version, target_version } = ruleVersions; const hasBaseVersion = base_version !== MissingVersion; @@ -78,11 +79,14 @@ export const calculateRuleFieldsDiff = ( // only for fields of a single rule type, and need to calculate it for all fields // of all the rule types we have. // TODO: Try to get rid of "as" casting - return calculateAllFieldsDiff({ - base_version: base_version as DiffableAllFields | MissingVersion, - current_version: current_version as DiffableAllFields, - target_version: target_version as DiffableAllFields, - }) as RuleFieldsDiff; + return calculateAllFieldsDiff( + { + base_version: base_version as DiffableAllFields | MissingVersion, + current_version: current_version as DiffableAllFields, + target_version: target_version as DiffableAllFields, + }, + isRuleCustomized + ) as RuleFieldsDiff; } switch (current_version.type) { @@ -93,7 +97,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'query', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateCustomQueryFieldsDiff({ base_version, current_version, target_version }), + ...calculateCustomQueryFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'saved_query': { @@ -103,7 +110,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'saved_query', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateSavedQueryFieldsDiff({ base_version, current_version, target_version }), + ...calculateSavedQueryFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'eql': { @@ -113,7 +123,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'eql', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateEqlFieldsDiff({ base_version, current_version, target_version }), + ...calculateEqlFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'threat_match': { @@ -123,7 +136,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'threat_match', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateThreatMatchFieldsDiff({ base_version, current_version, target_version }), + ...calculateThreatMatchFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'threshold': { @@ -133,7 +149,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'threshold', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateThresholdFieldsDiff({ base_version, current_version, target_version }), + ...calculateThresholdFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'machine_learning': { @@ -143,7 +162,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'machine_learning', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateMachineLearningFieldsDiff({ base_version, current_version, target_version }), + ...calculateMachineLearningFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'new_terms': { @@ -153,7 +175,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'new_terms', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateNewTermsFieldsDiff({ base_version, current_version, target_version }), + ...calculateNewTermsFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } case 'esql': { @@ -163,7 +188,10 @@ export const calculateRuleFieldsDiff = ( invariant(target_version.type === 'esql', TARGET_TYPE_ERROR); return { ...commonFieldsDiff, - ...calculateEsqlFieldsDiff({ base_version, current_version, target_version }), + ...calculateEsqlFieldsDiff( + { base_version, current_version, target_version }, + isRuleCustomized + ), }; } default: { @@ -173,9 +201,10 @@ export const calculateRuleFieldsDiff = ( }; const calculateCommonFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): CommonFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, commonFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, commonFieldsDiffAlgorithms, isRuleCustomized); }; const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -209,9 +238,10 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor }; const calculateCustomQueryFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): CustomQueryFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, customQueryFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, customQueryFieldsDiffAlgorithms, isRuleCustomized); }; const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -222,9 +252,10 @@ const customQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): SavedQueryFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, savedQueryFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, savedQueryFieldsDiffAlgorithms, isRuleCustomized); }; const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -235,9 +266,10 @@ const savedQueryFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): EqlFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, eqlFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, eqlFieldsDiffAlgorithms, isRuleCustomized); }; const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -248,9 +280,10 @@ const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { }; const calculateEsqlFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): EsqlFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, esqlFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, esqlFieldsDiffAlgorithms, isRuleCustomized); }; const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -260,9 +293,10 @@ const esqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { }; const calculateThreatMatchFieldsDiff = ( - ruleVersions: ThreeVersionsOf + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThreatMatchFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, threatMatchFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, threatMatchFieldsDiffAlgorithms, isRuleCustomized); }; const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -277,9 +311,10 @@ const threatMatchFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): ThresholdFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, thresholdFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, thresholdFieldsDiffAlgorithms, isRuleCustomized); }; const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -291,9 +326,14 @@ const thresholdFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): MachineLearningFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, machineLearningFieldsDiffAlgorithms); + return calculateFieldsDiffFor( + ruleVersions, + machineLearningFieldsDiffAlgorithms, + isRuleCustomized + ); }; const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = @@ -305,9 +345,10 @@ const machineLearningFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): NewTermsFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, newTermsFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, newTermsFieldsDiffAlgorithms, isRuleCustomized); }; const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { @@ -320,9 +361,10 @@ const newTermsFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + ruleVersions: ThreeVersionsOf, + isRuleCustomized: boolean ): AllFieldsDiff => { - return calculateFieldsDiffFor(ruleVersions, allFieldsDiffAlgorithms); + return calculateFieldsDiffFor(ruleVersions, allFieldsDiffAlgorithms, isRuleCustomized); }; const allFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts index eba89041260ce..9fe0a958eab2b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/diff_calculation_helpers.ts @@ -15,11 +15,12 @@ import { MissingVersion } from '../../../../../../../common/api/detection_engine export const calculateFieldsDiffFor = ( ruleVersions: ThreeVersionsOf, - fieldsDiffAlgorithms: FieldsDiffAlgorithmsFor + fieldsDiffAlgorithms: FieldsDiffAlgorithmsFor, + isRuleCustomized: boolean ): FieldsDiff => { const result = mapValues(fieldsDiffAlgorithms, (calculateFieldDiff, fieldName) => { const fieldVersions = pickField(fieldName as keyof TObject, ruleVersions); - const fieldDiff = calculateFieldDiff(fieldVersions); + const fieldDiff = calculateFieldDiff(fieldVersions, isRuleCustomized); return fieldDiff; }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts index cd778ad7a4e40..fc50342ac1786 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/references.ts @@ -297,10 +297,11 @@ export function referencesField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'references', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['http://url-3'], target: ['http://url-1', 'http://url-2'], - merged: ['http://url-1', 'http://url-2'], + merged: ['http://url-3', 'http://url-1', 'http://url-2'], }, }, getService diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts index 880ec3ed29f3a..ecaa093a7f359 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/common_fields/tags.ts @@ -297,10 +297,11 @@ export function tagsField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'tags', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['tagB'], target: ['tagC'], - merged: ['tagC'], + merged: ['tagB', 'tagC'], }, }, getService diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts index 48302fa6ef8a3..e59308c7b0b2b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/test_helpers.ts @@ -78,6 +78,7 @@ type ExpectedDiffOutcome = | { expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate; expectedFieldDiffValues: MissingHistoricalRuleVersionsFieldDiffValueVersions; + isMergableField?: boolean; }; /** @@ -160,6 +161,7 @@ export function testFieldUpgradeReview( expectMissingBaseABFieldDiff(diff, { diffableRuleFieldName: params.diffableRuleFieldName, valueVersions: params.expectedFieldDiffValues, + isMergableField: params.isMergableField, }); break; } @@ -495,6 +497,7 @@ function expectMissingBaseAAFieldDiff( interface MissingBaseFieldAssertParams { diffableRuleFieldName: string; valueVersions: MissingHistoricalRuleVersionsFieldDiffValueVersions; + isMergableField?: boolean; } /** @@ -518,7 +521,9 @@ function expectMissingBaseABFieldDiff( target_version: fieldAssertParams.valueVersions.target, merged_version: fieldAssertParams.valueVersions.merged, diff_outcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, - merge_outcome: ThreeWayMergeOutcome.Target, + merge_outcome: fieldAssertParams.isMergableField + ? ThreeWayMergeOutcome.Merged + : ThreeWayMergeOutcome.Target, conflict: ThreeWayDiffConflict.SOLVABLE, }, isUndefined diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts index 3af660439c1ee..14eb9b4fda39c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/new_terms_fields.ts @@ -302,10 +302,11 @@ export function newTermsFieldsField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'new_terms_fields', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['fieldB'], target: ['fieldA', 'fieldC'], - merged: ['fieldA', 'fieldC'], + merged: ['fieldB', 'fieldA', 'fieldC'], }, }, getService diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts index 41c6c6f7cf2fd..6f04b950861bb 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/diffable_rule_fields/type_specific_fields/threat_index.ts @@ -302,10 +302,11 @@ export function threatIndexField({ getService }: FtrProviderContext): void { ruleUpgradeAssets, diffableRuleFieldName: 'threat_index', expectedDiffOutcome: ThreeWayDiffOutcome.MissingBaseCanUpdate, + isMergableField: true, expectedFieldDiffValues: { current: ['indexD'], target: ['indexB', 'indexC'], - merged: ['indexB', 'indexC'], + merged: ['indexD', 'indexB', 'indexC'], }, }, getService diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts index 515ec4e633127..e011a983384fc 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts @@ -16,6 +16,7 @@ import { RULES_UPDATES_TABLE, UPGRADE_ALL_RULES_BUTTON, UPGRADE_SELECTED_RULES_BUTTON, + getReviewSingleRuleButtonByRuleId, getUpgradeSingleRuleButtonByRuleId, } from '../../../../screens/alerts_detection_rules'; import { selectRulesByName } from '../../../../tasks/alerts_detection_rules'; @@ -102,11 +103,11 @@ describe( clickRuleUpdatesTab(); }); - it('should disable individual upgrade buttons for all prebuilt rules with conflicts', () => { - // All buttons should be disabled because of conflicts + it('should display individual review buttons for all prebuilt rules with conflicts', () => { + // All buttons should be review buttons because of conflicts for (const rule of [OUTDATED_RULE_1, OUTDATED_RULE_2]) { const { rule_id: ruleId } = rule['security-rule']; - expect(cy.get(getUpgradeSingleRuleButtonByRuleId(ruleId)).should('be.disabled')); + expect(cy.get(getReviewSingleRuleButtonByRuleId(ruleId)).should('exist')); } }); @@ -203,24 +204,24 @@ describe( assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_3]); }); - it('should disable the upgrade button for conflicting rules while allowing upgrades of no-conflict rules', () => { - // Verify the conflicting rule's upgrade button is disabled + it('should switch to a review button for conflicting rules while allowing upgrades of no-conflict rules', () => { + // Verify the conflicting rule's upgrade button has the review label expect( cy - .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id)) - .should('be.disabled') + .get(getReviewSingleRuleButtonByRuleId(OUTDATED_RULE_1['security-rule'].rule_id)) + .should('exist') ); - // Verify non-conflicting rules' upgrade buttons are enabled + // Verify non-conflicting rules' upgrade buttons do not have the review label expect( cy .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_2['security-rule'].rule_id)) - .should('not.be.disabled') + .should('exist') ); expect( cy .get(getUpgradeSingleRuleButtonByRuleId(OUTDATED_RULE_3['security-rule'].rule_id)) - .should('not.be.disabled') + .should('exist') ); }); @@ -305,10 +306,10 @@ describe( }); it('should disable individual upgrade button for all rules', () => { - // All buttons should be disabled because rule type changes are considered conflicts + // All buttons should be displayed as review buttons because rule type changes are considered conflicts for (const rule of [OUTDATED_QUERY_RULE_1, OUTDATED_QUERY_RULE_2]) { const { rule_id: ruleId } = rule['security-rule']; - expect(cy.get(getUpgradeSingleRuleButtonByRuleId(ruleId)).should('be.disabled')); + expect(cy.get(getReviewSingleRuleButtonByRuleId(ruleId)).should('exist')); } }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts index 012de17a07b4f..cf9a9d84737ee 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts @@ -196,6 +196,10 @@ export const getUpgradeSingleRuleButtonByRuleId = (ruleId: string) => { return `[data-test-subj="upgradeSinglePrebuiltRuleButton-${ruleId}"]`; }; +export const getReviewSingleRuleButtonByRuleId = (ruleId: string) => { + return `[data-test-subj="reviewSinglePrebuiltRuleButton-${ruleId}"]`; +}; + export const NO_RULES_AVAILABLE_FOR_INSTALL_MESSAGE = '[data-test-subj="noPrebuiltRulesAvailableForInstall"]'; export const NO_RULES_AVAILABLE_FOR_UPGRADE_MESSAGE =