[9.0] [Security Solution] Allow bulk upgrade rules with solvable conflicts (#213285) (#213450)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Security Solution] Allow bulk upgrade rules with solvable conflicts
(#213285)](https://github.com/elastic/kibana/pull/213285)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Maxim
Palenov","email":"maxim.palenov@elastic.co"},"sourceCommit":{"committedDate":"2025-03-06T17:39:58Z","message":"[Security
Solution] Allow bulk upgrade rules with solvable conflicts
(#213285)\n\n**Partially addresses:**
https://github.com/elastic/kibana/issues/210358\n\n## Summary\n \nThis
PR implements functionality allowing users to bulk upgrade rules with
solvable conflicts.\n\n## Details\n\nThe main focus of this PR is to
allow users to bulk upgrade rules with solvable conflicts. To achieve
that the following was done\n\n- `upgrade/_perform` dry run
functionality was extended to take into account rule upgrade specifiers
with resolved value\n- `upgrade/_perform`'s `on_conflict` param was
extended with `UPGRADE_SOLVABLE` to allow bulk upgrading rules with
solvable conflicts\n- UI logic updated accordingly to display rule
upgrade modal when users have to make a choice to upgrade only rules
without conflicts or upgrade also rules with solvable conflicts\n-
conflict state badges were added to the rule upgrade table\n\nIt
includes changes from https://github.com/elastic/kibana/pull/213027 with
some modifications.\n\n## Screenshots\n\n<img width=\"1723\"
alt=\"Screenshot 2025-03-06 at 12 13 04\"
src=\"https://github.com/user-attachments/assets/b786e813-268d-49a2-80cc-81fa95d14e85\"
/>\n<img width=\"1724\" alt=\"Screenshot 2025-03-06 at 12 13 30\"
src=\"https://github.com/user-attachments/assets/e5e38bd9-78a3-4026-a7ea-892bd7153938\"
/>\n<img width=\"1723\" alt=\"Screenshot 2025-03-06 at 12 13 51\"
src=\"https://github.com/user-attachments/assets/d58872c3-f197-49ad-b4f3-5f45fb1efac2\"
/>\n<img width=\"1723\" alt=\"Screenshot 2025-03-06 at 12 14 04\"
src=\"https://github.com/user-attachments/assets/667a6ab2-2fdb-430d-9589-1c4a6e5cdc8b\"
/>\n<img width=\"1722\" alt=\"Screenshot 2025-03-06 at 12 14 17\"
src=\"https://github.com/user-attachments/assets/07f4cffe-4398-4fd5-8350-a3a2978d7dcd\"
/>","sha":"f2077dbb3108833e76a2f1834a76c3266e22f2ac","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","impact:high","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[Security
Solution] Allow bulk upgrade rules with solvable
conflicts","number":213285,"url":"https://github.com/elastic/kibana/pull/213285","mergeCommit":{"message":"[Security
Solution] Allow bulk upgrade rules with solvable conflicts
(#213285)\n\n**Partially addresses:**
https://github.com/elastic/kibana/issues/210358\n\n## Summary\n \nThis
PR implements functionality allowing users to bulk upgrade rules with
solvable conflicts.\n\n## Details\n\nThe main focus of this PR is to
allow users to bulk upgrade rules with solvable conflicts. To achieve
that the following was done\n\n- `upgrade/_perform` dry run
functionality was extended to take into account rule upgrade specifiers
with resolved value\n- `upgrade/_perform`'s `on_conflict` param was
extended with `UPGRADE_SOLVABLE` to allow bulk upgrading rules with
solvable conflicts\n- UI logic updated accordingly to display rule
upgrade modal when users have to make a choice to upgrade only rules
without conflicts or upgrade also rules with solvable conflicts\n-
conflict state badges were added to the rule upgrade table\n\nIt
includes changes from https://github.com/elastic/kibana/pull/213027 with
some modifications.\n\n## Screenshots\n\n<img width=\"1723\"
alt=\"Screenshot 2025-03-06 at 12 13 04\"
src=\"https://github.com/user-attachments/assets/b786e813-268d-49a2-80cc-81fa95d14e85\"
/>\n<img width=\"1724\" alt=\"Screenshot 2025-03-06 at 12 13 30\"
src=\"https://github.com/user-attachments/assets/e5e38bd9-78a3-4026-a7ea-892bd7153938\"
/>\n<img width=\"1723\" alt=\"Screenshot 2025-03-06 at 12 13 51\"
src=\"https://github.com/user-attachments/assets/d58872c3-f197-49ad-b4f3-5f45fb1efac2\"
/>\n<img width=\"1723\" alt=\"Screenshot 2025-03-06 at 12 14 04\"
src=\"https://github.com/user-attachments/assets/667a6ab2-2fdb-430d-9589-1c4a6e5cdc8b\"
/>\n<img width=\"1722\" alt=\"Screenshot 2025-03-06 at 12 14 17\"
src=\"https://github.com/user-attachments/assets/07f4cffe-4398-4fd5-8350-a3a2978d7dcd\"
/>","sha":"f2077dbb3108833e76a2f1834a76c3266e22f2ac"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213285","number":213285,"mergeCommit":{"message":"[Security
Solution] Allow bulk upgrade rules with solvable conflicts
(#213285)\n\n**Partially addresses:**
https://github.com/elastic/kibana/issues/210358\n\n## Summary\n \nThis
PR implements functionality allowing users to bulk upgrade rules with
solvable conflicts.\n\n## Details\n\nThe main focus of this PR is to
allow users to bulk upgrade rules with solvable conflicts. To achieve
that the following was done\n\n- `upgrade/_perform` dry run
functionality was extended to take into account rule upgrade specifiers
with resolved value\n- `upgrade/_perform`'s `on_conflict` param was
extended with `UPGRADE_SOLVABLE` to allow bulk upgrading rules with
solvable conflicts\n- UI logic updated accordingly to display rule
upgrade modal when users have to make a choice to upgrade only rules
without conflicts or upgrade also rules with solvable conflicts\n-
conflict state badges were added to the rule upgrade table\n\nIt
includes changes from https://github.com/elastic/kibana/pull/213027 with
some modifications.\n\n## Screenshots\n\n<img width=\"1723\"
alt=\"Screenshot 2025-03-06 at 12 13 04\"
src=\"https://github.com/user-attachments/assets/b786e813-268d-49a2-80cc-81fa95d14e85\"
/>\n<img width=\"1724\" alt=\"Screenshot 2025-03-06 at 12 13 30\"
src=\"https://github.com/user-attachments/assets/e5e38bd9-78a3-4026-a7ea-892bd7153938\"
/>\n<img width=\"1723\" alt=\"Screenshot 2025-03-06 at 12 13 51\"
src=\"https://github.com/user-attachments/assets/d58872c3-f197-49ad-b4f3-5f45fb1efac2\"
/>\n<img width=\"1723\" alt=\"Screenshot 2025-03-06 at 12 14 04\"
src=\"https://github.com/user-attachments/assets/667a6ab2-2fdb-430d-9589-1c4a6e5cdc8b\"
/>\n<img width=\"1722\" alt=\"Screenshot 2025-03-06 at 12 14 17\"
src=\"https://github.com/user-attachments/assets/07f4cffe-4398-4fd5-8350-a3a2978d7dcd\"
/>","sha":"f2077dbb3108833e76a2f1834a76c3266e22f2ac"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Maxim Palenov <maxim.palenov@elastic.co>
This commit is contained in:
Kibana Machine 2025-03-07 06:25:26 +11:00 committed by GitHub
parent e6f796c6ee
commit 5edf2d1b32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 574 additions and 112 deletions

View file

@ -8,7 +8,7 @@
import { z } from '@kbn/zod';
import { mapValues } from 'lodash';
import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen';
import { AggregatedPrebuiltRuleError, DiffableAllFields } from '../model';
import { AggregatedPrebuiltRuleError, DiffableAllFields, ThreeWayDiffConflict } from '../model';
import { RuleSignatureId, RuleVersion } from '../../model';
import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter';
@ -113,7 +113,7 @@ export const RuleUpgradeSpecifier = z.object({
});
export type UpgradeConflictResolution = z.infer<typeof UpgradeConflictResolution>;
export const UpgradeConflictResolution = z.enum(['SKIP', 'OVERWRITE']);
export const UpgradeConflictResolution = z.enum(['SKIP', 'UPGRADE_SOLVABLE']);
export type UpgradeConflictResolutionEnum = typeof UpgradeConflictResolution.enum;
export const UpgradeConflictResolutionEnum = UpgradeConflictResolution.enum;
@ -140,12 +140,25 @@ export const SkipRuleUpgradeReason = z.enum(['RULE_UP_TO_DATE', 'CONFLICT']);
export type SkipRuleUpgradeReasonEnum = typeof SkipRuleUpgradeReason.enum;
export const SkipRuleUpgradeReasonEnum = SkipRuleUpgradeReason.enum;
export type SkippedRuleUpgrade = z.infer<typeof SkippedRuleUpgrade>;
export const SkippedRuleUpgrade = z.object({
export type RuleUpToDateSkipReason = z.infer<typeof RuleUpToDateSkipReason>;
export const RuleUpToDateSkipReason = z.object({
reason: z.literal(SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE),
rule_id: z.string(),
reason: SkipRuleUpgradeReason,
});
export type UpgradeConflictSkipReason = z.infer<typeof UpgradeConflictSkipReason>;
export const UpgradeConflictSkipReason = z.object({
reason: z.literal(SkipRuleUpgradeReasonEnum.CONFLICT),
rule_id: z.string(),
conflict: z.nativeEnum(ThreeWayDiffConflict),
});
export type SkippedRuleUpgrade = z.infer<typeof SkippedRuleUpgrade>;
export const SkippedRuleUpgrade = z.discriminatedUnion('reason', [
RuleUpToDateSkipReason,
UpgradeConflictSkipReason,
]);
export type PerformRuleUpgradeResponseBody = z.infer<typeof PerformRuleUpgradeResponseBody>;
export const PerformRuleUpgradeResponseBody = z.object({
summary: z.object({

View file

@ -575,7 +575,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
<EuiConfirmModal
title={ruleI18n.SINGLE_DELETE_CONFIRMATION_TITLE}
onCancel={handleDeletionCancel}
onConfirm={handleDeletionConfirm}
onConfirm={() => handleDeletionConfirm()}
confirmButtonText={ruleI18n.DELETE_CONFIRMATION_CONFIRM}
cancelButtonText={ruleI18n.DELETE_CONFIRMATION_CANCEL}
buttonColor="danger"

View file

@ -15,16 +15,23 @@ import { useIsUpgradingSecurityPackages } from '../logic/use_upgrade_security_pa
import { usePrebuiltRulesCustomizationStatus } from '../logic/prebuilt_rules/use_prebuilt_rules_customization_status';
import { usePerformUpgradeRules } from '../logic/prebuilt_rules/use_perform_rule_upgrade';
import { usePrebuiltRulesUpgradeReview } from '../logic/prebuilt_rules/use_prebuilt_rules_upgrade_review';
import type {
FindRulesSortField,
RuleFieldsToUpgrade,
RuleResponse,
RuleSignatureId,
RuleUpgradeSpecifier,
import type { PerformRuleUpgradeRequestBody } from '../../../../common/api/detection_engine';
import {
type FindRulesSortField,
type RuleFieldsToUpgrade,
type RuleResponse,
type RuleSignatureId,
type RuleUpgradeSpecifier,
ThreeWayDiffConflict,
SkipRuleUpgradeReasonEnum,
UpgradeConflictResolutionEnum,
} from '../../../../common/api/detection_engine';
import { usePrebuiltRulesUpgradeState } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
import { useOutdatedMlJobsUpgradeModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_ml_jobs_upgrade_modal';
import { useUpgradeWithConflictsModal } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal';
import {
ConfirmRulesUpgrade,
useUpgradeWithConflictsModal,
} from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal';
import * as ruleDetailsI18n from '../components/rule_details/translations';
import * as i18n from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations';
import { UpgradeFlyoutSubHeader } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader';
@ -36,6 +43,7 @@ import { RuleDiffTab } from '../components/rule_details/rule_diff_tab';
import { useRulePreviewFlyout } from '../../rule_management_ui/components/rules_table/use_rule_preview_flyout';
import type { UpgradePrebuiltRulesSortingOptions } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context';
import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../../rule_management_ui/components/rules_table/constants';
import type { RulesConflictStats } from '../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_with_conflicts_modal/upgrade_modal';
const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000;
@ -100,26 +108,18 @@ export function usePrebuiltRulesUpgrade({
const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal();
const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules();
const upgradeRulesWithDryRun = useRulesUpgradeWithDryRun(confirmConflictsUpgrade);
const upgradeRulesToResolved = useCallback(
async (ruleIds: RuleSignatureId[]) => {
const conflictRuleIdsSet = new Set(
ruleIds.filter(
(ruleId) =>
rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 &&
rulesUpgradeState[ruleId].hasUnresolvedConflicts
)
);
const upgradingRuleIds = ruleIds.filter((ruleId) => !conflictRuleIdsSet.has(ruleId));
const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = upgradingRuleIds.map((ruleId) => ({
const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds.map((ruleId) => ({
rule_id: ruleId,
version: rulesUpgradeState[ruleId].target_rule.version,
revision: rulesUpgradeState[ruleId].revision,
fields: constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]),
}));
setLoadingRules((prev) => [...prev, ...upgradingRuleIds]);
setLoadingRules((prev) => [...prev, ...ruleIds]);
try {
// Handle MLJobs modal
@ -127,11 +127,7 @@ export function usePrebuiltRulesUpgrade({
return;
}
if (conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade())) {
return;
}
await upgradeRulesRequest({
await upgradeRulesWithDryRun({
mode: 'SPECIFIC_RULES',
pick_version: 'MERGED',
rules: ruleUpgradeSpecifiers,
@ -139,7 +135,7 @@ export function usePrebuiltRulesUpgrade({
} catch {
// Error is handled by the mutation's onError callback, so no need to do anything here
} finally {
const upgradedRuleIdsSet = new Set(upgradingRuleIds);
const upgradedRuleIdsSet = new Set(ruleIds);
if (onUpgrade) {
onUpgrade();
@ -148,13 +144,7 @@ export function usePrebuiltRulesUpgrade({
setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id)));
}
},
[
rulesUpgradeState,
confirmLegacyMLJobs,
confirmConflictsUpgrade,
upgradeRulesRequest,
onUpgrade,
]
[rulesUpgradeState, confirmLegacyMLJobs, upgradeRulesWithDryRun, onUpgrade]
);
const upgradeRulesToTarget = useCallback(
@ -213,27 +203,10 @@ export function usePrebuiltRulesUpgrade({
return;
}
const dryRunResults = await upgradeRulesRequest({
await upgradeRulesWithDryRun({
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
@ -242,11 +215,10 @@ export function usePrebuiltRulesUpgrade({
}
}, [
upgradeableRules,
upgradeRulesWithDryRun,
confirmLegacyMLJobs,
upgradeRulesRequest,
isRulesCustomizationEnabled,
filter,
confirmConflictsUpgrade,
]);
const subHeaderFactory = useCallback(
@ -405,6 +377,68 @@ export function usePrebuiltRulesUpgrade({
};
}
/**
* Upgrades rules in two steps
* - first fires a dry run request to check for rule upgrade conflicts. If there are conflicts
* it calls `confirmConflictsUpgrade()` and await its result.
* - second it either fires a request to upgrade rules or exits depending on user's choice
*/
function useRulesUpgradeWithDryRun(
confirmConflictsUpgrade: (
conflictsStats: RulesConflictStats
) => Promise<ConfirmRulesUpgrade | boolean>
) {
const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules();
return useCallback(
async (requestParams: PerformRuleUpgradeRequestBody) => {
const dryRunResults = await upgradeRulesRequest({
...requestParams,
dry_run: true,
on_conflict: UpgradeConflictResolutionEnum.SKIP,
});
const numOfRulesWithSolvableConflicts = dryRunResults.results.skipped.filter(
(x) =>
x.reason === SkipRuleUpgradeReasonEnum.CONFLICT &&
x.conflict === ThreeWayDiffConflict.SOLVABLE
).length;
const numOfRulesWithNonSolvableConflicts = dryRunResults.results.skipped.filter(
(x) =>
x.reason === SkipRuleUpgradeReasonEnum.CONFLICT &&
x.conflict === ThreeWayDiffConflict.NON_SOLVABLE
).length;
if (numOfRulesWithSolvableConflicts === 0 && numOfRulesWithNonSolvableConflicts === 0) {
// There are no rule with conflicts
await upgradeRulesRequest({
...requestParams,
on_conflict: UpgradeConflictResolutionEnum.SKIP,
});
} else {
const result = await confirmConflictsUpgrade({
numOfRulesWithoutConflicts: dryRunResults.results.updated.length,
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
});
if (!result) {
return;
}
await upgradeRulesRequest({
...requestParams,
on_conflict:
result === ConfirmRulesUpgrade.WithSolvableConflicts
? UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE
: UpgradeConflictResolutionEnum.SKIP,
});
}
},
[upgradeRulesRequest, confirmConflictsUpgrade]
);
}
function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade {
const ruleFieldsToUpgrade: Record<string, unknown> = {};

View file

@ -5,10 +5,17 @@
* 2.0.
*/
import { type RuleUpgradeInfoForReview } from '../../../../../common/api/detection_engine';
import {
type ThreeWayDiffConflict,
type RuleUpgradeInfoForReview,
} from '../../../../../common/api/detection_engine';
import type { FieldsUpgradeState } from './fields_upgrade_state';
export interface RuleUpgradeState extends RuleUpgradeInfoForReview {
/**
* Original rule conflict state calculated from fields diff.
*/
conflict: ThreeWayDiffConflict;
/**
* Stores a record of customizable field names mapped to field upgrade state.
*/

View file

@ -7,9 +7,9 @@
import { useCallback, useRef } from 'react';
type UseAsyncConfirmationReturn = [
initConfirmation: () => Promise<boolean>,
confirm: () => void,
type UseAsyncConfirmationReturn<ConfirmResult = unknown> = [
initConfirmation: () => Promise<ConfirmResult | boolean>,
confirm: (result?: ConfirmResult) => void,
cancel: () => void
];
@ -19,14 +19,15 @@ interface UseAsyncConfirmationArgs {
}
// TODO move to common hooks
export const useAsyncConfirmation = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useAsyncConfirmation = <ConfirmResult = any>({
onInit,
onFinish,
}: UseAsyncConfirmationArgs): UseAsyncConfirmationReturn => {
const confirmationPromiseRef = useRef<(result: boolean) => void>();
}: UseAsyncConfirmationArgs): UseAsyncConfirmationReturn<ConfirmResult> => {
const confirmationPromiseRef = useRef<(result: ConfirmResult | boolean) => void>();
const confirm = useCallback(() => {
confirmationPromiseRef.current?.(true);
const confirm = useCallback((result?: ConfirmResult) => {
confirmationPromiseRef.current?.(result ?? true);
}, []);
const cancel = useCallback(() => {
@ -36,7 +37,7 @@ export const useAsyncConfirmation = ({
const initConfirmation = useCallback(() => {
onInit();
return new Promise<boolean>((resolve) => {
return new Promise<ConfirmResult | boolean>((resolve) => {
confirmationPromiseRef.current = resolve;
}).finally(() => {
onFinish();

View file

@ -275,7 +275,7 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
: i18n.BULK_DELETE_CONFIRMATION_TITLE
}
onCancel={handleDeletionCancel}
onConfirm={handleDeletionConfirm}
onConfirm={() => handleDeletionConfirm()}
confirmButtonText={i18n.DELETE_CONFIRMATION_CONFIRM}
cancelButtonText={i18n.DELETE_CONFIRMATION_CANCEL}
buttonColor="danger"

View file

@ -36,7 +36,9 @@ export const UpgradePrebuiltRulesTableButtons = ({
const doAllSelectedRulesHaveConflicts =
isRulesCustomizationEnabled &&
selectedRules.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts);
selectedRules.every(
({ hasNonSolvableUnresolvedConflicts }) => hasNonSolvableUnresolvedConflicts
);
const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({
canUserEditRules,

View file

@ -140,6 +140,7 @@ export function usePrebuiltRulesUpgradeState(
state[ruleUpgradeInfo.rule_id] = {
...ruleUpgradeInfo,
conflict: getWorstConflictLevelAmongFields(ruleUpgradeInfo.diff.fields),
fieldsUpgradeState,
hasUnresolvedConflicts: isRulesCustomizationEnabled
? hasRuleTypeChange || hasFieldConflicts
@ -212,3 +213,22 @@ function calcFieldsState(
return fieldsState;
}
function getWorstConflictLevelAmongFields(
fieldsDiff: FieldsDiff<Record<string, unknown>>
): ThreeWayDiffConflict {
let mostSevereFieldConflict = ThreeWayDiffConflict.NONE;
for (const { conflict } of Object.values<{ conflict: ThreeWayDiffConflict }>(fieldsDiff)) {
if (conflict === ThreeWayDiffConflict.NON_SOLVABLE) {
// return early as there is no higher severity
return ThreeWayDiffConflict.NON_SOLVABLE;
}
if (conflict === ThreeWayDiffConflict.SOLVABLE) {
mostSevereFieldConflict = ThreeWayDiffConflict.SOLVABLE;
}
}
return mostSevereFieldConflict;
}

View file

@ -15,6 +15,7 @@ import {
EuiToolTip,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import { ThreeWayDiffConflict } from '../../../../../../common/api/detection_engine';
import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state';
import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name';
import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants';
@ -142,6 +143,43 @@ const MODIFIED_COLUMN: TableColumn = {
truncateText: true,
};
const CONFLICT_COLUMN: TableColumn = {
field: 'conflict',
name: <RulesTableEmptyColumnName name={i18n.COLUMN_CONFLICT} />,
align: 'center',
render: (conflict: ThreeWayDiffConflict) => {
switch (conflict) {
case ThreeWayDiffConflict.SOLVABLE:
return (
<EuiToolTip content={i18n.SOLVABLE_CONFLICT_TOOLTIP}>
<EuiBadge
color="warning"
data-test-subj="upgradeRulesTableSolvableConflictColumnBadge"
aria-label={i18n.SOLVABLE_CONFLICT_LABEL}
>
{i18n.SOLVABLE_CONFLICT_LABEL}
</EuiBadge>
</EuiToolTip>
);
case ThreeWayDiffConflict.NON_SOLVABLE:
return (
<EuiToolTip content={i18n.NON_SOLVABLE_CONFLICT_TOOLTIP}>
<EuiBadge
color="danger"
data-test-subj="upgradeRulesTableUnsolvableConflictColumnBadge"
aria-label={i18n.NON_SOLVABLE_CONFLICT_LABEL}
>
{i18n.NON_SOLVABLE_CONFLICT_LABEL}
</EuiBadge>
</EuiToolTip>
);
}
},
width: '170px',
truncateText: true,
};
const createUpgradeButtonColumn = (
upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'],
openRulePreview: UpgradePrebuiltRulesTableActions['openRulePreview'],
@ -167,6 +205,7 @@ const createUpgradeButtonColumn = (
return (
<EuiToolTip content={i18n.UPDATE_RULE_BUTTON_TOOLTIP_CONFLICTS}>
<EuiButtonEmpty
color="warning"
size="s"
disabled={isUpgradeButtonDisabled}
onClick={() => openRulePreview(ruleId)}
@ -217,6 +256,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
() => [
RULE_NAME_COLUMN,
...(shouldShowModifiedColumn ? [MODIFIED_COLUMN] : []),
CONFLICT_COLUMN,
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
@ -238,7 +278,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
sortable: ({ current_rule: { severity } }: RuleUpgradeState) =>
getNormalizedSeverity(severity),
truncateText: true,
width: '12%',
width: '10%',
},
...(hasCRUDPermissions
? [

View file

@ -5,12 +5,14 @@
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
export const UPGRADE_CONFLICTS_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeConflictsModal.messageTitle',
{
defaultMessage: 'Update rules without conflicts?',
defaultMessage: 'There are rules with conflicts',
}
);
@ -21,17 +23,135 @@ export const UPGRADE_CONFLICTS_MODAL_CANCEL = i18n.translate(
}
);
export const UPGRADE_CONFLICTS_MODAL_CONFIRM = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeConflictsModal.confirmTitle',
export const UPGRADE_RULES_WITHOUT_CONFLICTS = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeConflictsModal.upgradeRulesWithoutConflicts',
{
defaultMessage: 'Update rules without conflicts',
}
);
export const UPGRADE_CONFLICTS_MODAL_BODY = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeConflictsModal.affectedJobsTitle',
export const UPGRADE_RULES_WITH_CONFLICTS = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeConflictsModal.upgradeRulesWithConflicts',
{
defaultMessage:
"Some of the selected rules have conflicts and, for that reason, won't be updated. Resolve the conflicts to properly update the rules.",
defaultMessage: 'Update rules',
}
);
export const ONLY_RULES_WITH_SOLVABLE_CONFLICTS = (numOfRules: number) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.onlySolvableConflicts"
defaultMessage="{numOfRulesStrong} selected {numOfRules, plural, =1 {rule has} other {rules have}} auto-resolved conflicts. You may proceed updating without reviewing conflicts but this operation is potentially dangerous and may result in broken rules."
values={{ numOfRules, numOfRulesStrong: <strong>{numOfRules}</strong> }}
/>
);
export const ONLY_RULES_WITH_NON_SOLVABLE_CONFLICTS = (numOfRules: number) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.onlyNonSolvableConflicts"
defaultMessage="{numOfRulesStrong} selected {numOfRules, plural, =1 {rule has} other {rules have}} unresolved conflicts. Please review and update them individually via the flyout."
values={{ numOfRules, numOfRulesStrong: <strong>{numOfRules}</strong> }}
/>
);
export const ONLY_RULES_WITH_CONFLICTS = ({
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
}: {
numOfRulesWithSolvableConflicts: number;
numOfRulesWithNonSolvableConflicts: number;
}) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.rulesWithoutConflictsAndRulesWithNonSolvableConflicts"
defaultMessage="{numOfRulesStrong} selected {numOfRules, plural, =1 {rule has} other {rules have}} conflicts. You may proceed updating {numOfRulesWithSolvableConflictsStrong} {numOfRulesWithSolvableConflicts, plural, =1 {rule} other {rules}} with auto-resolved conflicts but this operation is potentially dangerous and may result in broken rules. Rules with unresolved conflicts may be updated only via the flyout."
values={{
numOfRules: numOfRulesWithSolvableConflicts + numOfRulesWithNonSolvableConflicts,
numOfRulesStrong: (
<strong>{numOfRulesWithSolvableConflicts + numOfRulesWithNonSolvableConflicts}</strong>
),
numOfRulesWithSolvableConflicts,
numOfRulesWithSolvableConflictsStrong: <strong>{numOfRulesWithSolvableConflicts}</strong>,
}}
/>
);
export const RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_NON_SOLVABLE_CONFLICTS = ({
numOfRulesWithoutConflicts,
numOfRulesWithNonSolvableConflicts,
}: {
numOfRulesWithoutConflicts: number;
numOfRulesWithNonSolvableConflicts: number;
}) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.rulesWithoutConflictsAndRulesWithNonSolvableConflicts"
defaultMessage="{numOfRulesWithNonSolvableConflicts} of {numOfRulesStrong} selected {numOfRules, plural, =1 {rule has} other {rules have}} unresolved conflicts. You may proceed updating only {numOfRulesWithoutConflictsStrong} {numOfRulesWithoutConflicts, plural, =1 {rule} other {rules}} without conflicts. Rules with unresolved conflicts may be updated only via the flyout."
values={{
numOfRules: numOfRulesWithoutConflicts + numOfRulesWithNonSolvableConflicts,
numOfRulesStrong: (
<strong>{numOfRulesWithoutConflicts + numOfRulesWithNonSolvableConflicts}</strong>
),
numOfRulesWithoutConflicts,
numOfRulesWithoutConflictsStrong: <strong>{numOfRulesWithoutConflicts}</strong>,
numOfRulesWithNonSolvableConflicts: <strong>{numOfRulesWithNonSolvableConflicts}</strong>,
}}
/>
);
export const RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_SOLVABLE_CONFLICTS = ({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
}: {
numOfRulesWithoutConflicts: number;
numOfRulesWithSolvableConflicts: number;
}) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.rulesWithoutConflictsAndRulesWithSolvableConflicts"
defaultMessage="{numOfRulesWithSolvableConflictsStrong} of {numOfRulesStrong} selected rules have auto-resolved conflicts. You may proceed and update only {numOfRulesWithoutConflictsStrong} {numOfRulesWithoutConflicts, plural, =1 {rule} other {rules}} without conflicts or update all rules which is potentially dangerous and may result in broken rules."
values={{
numOfRulesStrong: (
<strong>{numOfRulesWithoutConflicts + numOfRulesWithSolvableConflicts}</strong>
),
numOfRulesWithoutConflicts,
numOfRulesWithoutConflictsStrong: <strong>{numOfRulesWithoutConflicts}</strong>,
numOfRulesWithSolvableConflictsStrong: <strong>{numOfRulesWithSolvableConflicts}</strong>,
}}
/>
);
export const ALL_KINDS_OF_RULES = ({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
}: {
numOfRulesWithoutConflicts: number;
numOfRulesWithSolvableConflicts: number;
numOfRulesWithNonSolvableConflicts: number;
}) => (
<>
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.allKindsOfRules"
defaultMessage="{numOfRulesWithConflictsStrong} of {numOfRulesStrong} selected rules have conflicts. You may proceed and update only {numOfRulesWithoutConflictsStrong} {numOfRulesWithoutConflicts, plural, =1 {rule} other {rules}} without conflicts or update also {numOfRulesWithSolvableConflictsStrong} {numOfRulesWithSolvableConflicts, plural, =1 {rule} other {rules}} with auto-resolved conflicts which is potentially dangerous and may result in broken rules."
values={{
numOfRulesStrong: (
<strong>
{numOfRulesWithoutConflicts +
numOfRulesWithSolvableConflicts +
numOfRulesWithNonSolvableConflicts}
</strong>
),
numOfRulesWithConflictsStrong: (
<strong>{numOfRulesWithSolvableConflicts + numOfRulesWithNonSolvableConflicts}</strong>
),
numOfRulesWithoutConflicts,
numOfRulesWithoutConflictsStrong: <strong>{numOfRulesWithoutConflicts}</strong>,
numOfRulesWithSolvableConflicts,
numOfRulesWithSolvableConflictsStrong: <strong>{numOfRulesWithSolvableConflicts}</strong>,
}}
/>
{numOfRulesWithNonSolvableConflicts > 0 && (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.upgradeConflictsModal.unresolvedConflicts"
defaultMessage="Rules with unresolved conflicts may be updated only via the flyout."
/>
)}
</>
);

View file

@ -5,31 +5,122 @@
* 2.0.
*/
import { EuiConfirmModal, EuiText } from '@elastic/eui';
import React, { memo } from 'react';
import {
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiButton,
EuiButtonEmpty,
EuiText,
} from '@elastic/eui';
import React, { memo, useCallback } from 'react';
import { ConfirmRulesUpgrade } from './use_upgrade_modal';
import * as i18n from './translations';
interface UpgradeWithConflictsModalProps {
export interface RulesConflictStats {
numOfRulesWithoutConflicts: number;
numOfRulesWithSolvableConflicts: number;
numOfRulesWithNonSolvableConflicts: number;
}
interface UpgradeWithConflictsModalProps extends RulesConflictStats {
onCancel: () => void;
onConfirm: () => void;
onConfirm: (result: ConfirmRulesUpgrade) => void;
}
export const UpgradeWithConflictsModal = memo(function ConfirmUpgradeWithConflictsModal({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
onCancel,
onConfirm,
}: UpgradeWithConflictsModalProps): JSX.Element {
const confirmUpgradingRulesWithoutConflicts = useCallback(
() => onConfirm(ConfirmRulesUpgrade.WithoutConflicts),
[onConfirm]
);
const confirmUpgradingRulesWithSolvableConflicts = useCallback(
() => onConfirm(ConfirmRulesUpgrade.WithSolvableConflicts),
[onConfirm]
);
return (
<EuiConfirmModal
title={i18n.UPGRADE_CONFLICTS_MODAL_TITLE}
onCancel={onCancel}
onConfirm={onConfirm}
cancelButtonText={i18n.UPGRADE_CONFLICTS_MODAL_CANCEL}
confirmButtonText={i18n.UPGRADE_CONFLICTS_MODAL_CONFIRM}
buttonColor="primary"
defaultFocusedButton="confirm"
data-test-subj="upgradeConflictsModal"
>
<EuiText>{i18n.UPGRADE_CONFLICTS_MODAL_BODY}</EuiText>
</EuiConfirmModal>
<EuiModal data-test-subj="upgradeConflictsModal" onClose={onCancel}>
<EuiModalHeader>
<EuiModalHeaderTitle>{i18n.UPGRADE_CONFLICTS_MODAL_TITLE}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
{getModalBodyText({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
})}
</EuiText>
</EuiModalBody>
<EuiModalFooter>
{numOfRulesWithoutConflicts > 0 && (
<EuiButton onClick={confirmUpgradingRulesWithoutConflicts}>
{i18n.UPGRADE_RULES_WITHOUT_CONFLICTS}
</EuiButton>
)}
{numOfRulesWithSolvableConflicts > 0 && (
<EuiButton onClick={confirmUpgradingRulesWithSolvableConflicts} color="warning">
{i18n.UPGRADE_RULES_WITH_CONFLICTS}
</EuiButton>
)}
<EuiButtonEmpty onClick={onCancel}>{i18n.UPGRADE_CONFLICTS_MODAL_CANCEL}</EuiButtonEmpty>
</EuiModalFooter>
</EuiModal>
);
});
function getModalBodyText({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
}: RulesConflictStats): JSX.Element {
// Only solvable conflicts
if (numOfRulesWithoutConflicts === 0 && numOfRulesWithNonSolvableConflicts === 0) {
return i18n.ONLY_RULES_WITH_SOLVABLE_CONFLICTS(numOfRulesWithSolvableConflicts);
}
// Only non-solvable conflicts
if (numOfRulesWithoutConflicts === 0 && numOfRulesWithSolvableConflicts === 0) {
return i18n.ONLY_RULES_WITH_NON_SOLVABLE_CONFLICTS(numOfRulesWithNonSolvableConflicts);
}
// Only conflicts
if (numOfRulesWithoutConflicts === 0) {
return i18n.ONLY_RULES_WITH_CONFLICTS({
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
});
}
// Rules without conflicts + rules with solvable conflicts
if (numOfRulesWithNonSolvableConflicts === 0) {
return i18n.RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_SOLVABLE_CONFLICTS({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
});
}
// Rules without conflicts + rules with non-solvable conflicts
if (numOfRulesWithSolvableConflicts === 0) {
return i18n.RULES_WITHOUT_CONFLICTS_AND_RULES_WITH_NON_SOLVABLE_CONFLICTS({
numOfRulesWithoutConflicts,
numOfRulesWithNonSolvableConflicts,
});
}
return i18n.ALL_KINDS_OF_RULES({
numOfRulesWithoutConflicts,
numOfRulesWithSolvableConflicts,
numOfRulesWithNonSolvableConflicts,
});
}

View file

@ -6,25 +6,53 @@
*/
import type { ReactNode } from 'react';
import React from 'react';
import React, { useCallback, useState } from 'react';
import { useBoolean } from '@kbn/react-hooks';
import { useAsyncConfirmation } from '../../rules_table/use_async_confirmation';
import type { RulesConflictStats } from './upgrade_modal';
import { UpgradeWithConflictsModal } from './upgrade_modal';
export enum ConfirmRulesUpgrade {
WithoutConflicts = 'WithoutConflicts',
WithSolvableConflicts = 'WithSolvableConflicts',
}
interface UseUpgradeWithConflictsModalResult {
modal: ReactNode;
confirmConflictsUpgrade: () => Promise<boolean>;
confirmConflictsUpgrade: (
conflictsStats: RulesConflictStats
) => Promise<ConfirmRulesUpgrade | boolean>;
}
export function useUpgradeWithConflictsModal(): UseUpgradeWithConflictsModalResult {
const [isVisible, { on: showModal, off: hideModal }] = useBoolean(false);
const [confirmConflictsUpgrade, confirm, cancel] = useAsyncConfirmation({
const [initConfirmation, confirm, cancel] = useAsyncConfirmation<ConfirmRulesUpgrade>({
onInit: showModal,
onFinish: hideModal,
});
const [rulesUpgradeConflictsStats, setRulesUpgradeConflictsStats] = useState<RulesConflictStats>({
numOfRulesWithoutConflicts: 0,
numOfRulesWithSolvableConflicts: 0,
numOfRulesWithNonSolvableConflicts: 0,
});
const confirmConflictsUpgrade = useCallback(
(conflictsStats: RulesConflictStats) => {
setRulesUpgradeConflictsStats(conflictsStats);
return initConfirmation();
},
[initConfirmation]
);
return {
modal: isVisible && <UpgradeWithConflictsModal onConfirm={confirm} onCancel={cancel} />,
modal: isVisible && (
<UpgradeWithConflictsModal
{...rulesUpgradeConflictsStats}
onConfirm={confirm}
onCancel={cancel}
/>
),
confirmConflictsUpgrade,
};
}

View file

@ -702,6 +702,13 @@ export const COLUMN_MODIFIED = i18n.translate(
}
);
export const COLUMN_CONFLICT = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.columns.conflictTitle',
{
defaultMessage: 'Conflict',
}
);
export const COLUMN_ENABLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.columns.enabledTitle',
{
@ -863,6 +870,36 @@ export const RULE_EXECUTION_STATUS_FILTER = i18n.translate(
}
);
export const SOLVABLE_CONFLICT_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.solvableConflictLabel',
{
defaultMessage: 'Auto-resolved conflict',
}
);
export const SOLVABLE_CONFLICT_TOOLTIP = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.solvableConflictTooltipDescription',
{
defaultMessage:
'This Elastic rule has auto-resolved conflicts that require review before upgrade.',
}
);
export const NON_SOLVABLE_CONFLICT_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.nonSolvableConflictLabel',
{
defaultMessage: 'Unresolved conflict',
}
);
export const NON_SOLVABLE_CONFLICT_TOOLTIP = i18n.translate(
'xpack.securitySolution.detectionEngine.upgradeRules.nonSolvableConflictTooltipDescription',
{
defaultMessage:
'This Elastic rule has unresolved conflicts that require editing before upgrade.',
}
);
export const NO_RULES = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesTitle',
{
@ -1451,14 +1488,14 @@ export const INSTALL_RULE_BUTTON = i18n.translate(
export const UPDATE_RULE_BUTTON = i18n.translate(
'xpack.securitySolution.addRules.upgradeRuleButton',
{
defaultMessage: 'Update rule',
defaultMessage: 'Update',
}
);
export const REVIEW_RULE_BUTTON = i18n.translate(
'xpack.securitySolution.addRules.reviewRuleButton',
{
defaultMessage: 'Review rule',
defaultMessage: 'Review',
}
);

View file

@ -14,6 +14,8 @@ import {
type AllFieldsDiff,
MissingVersion,
} from '../../../../../../common/api/detection_engine';
import type { UpgradeConflictResolution } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import { UpgradeConflictResolutionEnum } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { assertPickVersionIsTarget } from './assert_pick_version_is_target';
@ -40,7 +42,11 @@ export const createModifiedPrebuiltRuleAssets = ({
defaultPickVersion,
}: CreateModifiedPrebuiltRuleAssetsProps) => {
return withSecuritySpanSync(createModifiedPrebuiltRuleAssets.name, () => {
const { pick_version: globalPickVersion = defaultPickVersion, mode } = requestBody;
const {
pick_version: globalPickVersion = defaultPickVersion,
mode,
on_conflict: onConflict,
} = requestBody;
const { modifiedPrebuiltRuleAssets, processingErrors } =
upgradeableRules.reduce<ProcessedRules>(
@ -77,7 +83,9 @@ export const createModifiedPrebuiltRuleAssets = ({
) as AllFieldsDiff;
if (mode === 'ALL_RULES' && globalPickVersion === 'MERGED') {
const fieldsWithConflicts = Object.keys(getFieldsDiffConflicts(calculatedRuleDiff));
const fieldsWithConflicts = Object.keys(
getFieldsDiffConflicts(calculatedRuleDiff, onConflict)
);
if (fieldsWithConflicts.length > 0) {
// If the mode is ALL_RULES, no fields can be overriden to any other pick_version
// than "MERGED", so throw an error for the fields that have conflicts.
@ -152,7 +160,12 @@ function createModifiedPrebuiltRuleAsset({
return modifiedPrebuiltRuleAsset as PrebuiltRuleAsset;
}
const getFieldsDiffConflicts = (ruleFieldsDiff: Partial<AllFieldsDiff>) =>
pickBy(ruleFieldsDiff, (diff) => {
return diff.conflict !== 'NONE';
});
const getFieldsDiffConflicts = (
ruleFieldsDiff: Partial<AllFieldsDiff>,
onConflict?: UpgradeConflictResolution
) =>
pickBy(ruleFieldsDiff, (diff) =>
onConflict === UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE
? diff.conflict !== 'NONE' && diff.conflict !== 'SOLVABLE'
: diff.conflict !== 'NONE'
);

View file

@ -47,6 +47,7 @@ export const getValueForField = ({
pick_version: globalPickVersion,
},
ruleFieldsDiff,
onConflict: requestBody.on_conflict,
})
: getValueFromRuleTriad({
fieldName,
@ -85,6 +86,7 @@ export const getValueForField = ({
upgradeableRule,
fieldUpgradeSpecifier,
ruleFieldsDiff,
onConflict: requestBody.on_conflict,
})
: getValueFromRuleTriad({
fieldName,

View file

@ -5,9 +5,11 @@
* 2.0.
*/
import type {
RuleFieldsToUpgrade,
AllFieldsDiff,
import {
type RuleFieldsToUpgrade,
type AllFieldsDiff,
type UpgradeConflictResolution,
UpgradeConflictResolutionEnum,
} from '../../../../../../common/api/detection_engine';
import { RULE_DEFAULTS } from '../../../rule_management/logic/detection_rules_client/mergers/apply_rule_defaults';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
@ -24,11 +26,13 @@ export const getValueFromMergedVersion = ({
upgradeableRule,
fieldUpgradeSpecifier,
ruleFieldsDiff,
onConflict,
}: {
fieldName: keyof PrebuiltRuleAsset;
upgradeableRule: RuleTriad;
fieldUpgradeSpecifier: NonNullable<RuleFieldsToUpgrade[keyof RuleFieldsToUpgrade]>;
ruleFieldsDiff: AllFieldsDiff;
onConflict?: UpgradeConflictResolution;
}) => {
const ruleId = upgradeableRule.target.rule_id;
const diffableRuleFieldName = mapRuleFieldToDiffableRuleField({
@ -39,7 +43,11 @@ export const getValueFromMergedVersion = ({
if (fieldUpgradeSpecifier.pick_version === 'MERGED') {
const ruleFieldDiff = ruleFieldsDiff[diffableRuleFieldName];
if (ruleFieldDiff && ruleFieldDiff.conflict !== 'NONE') {
if (
ruleFieldDiff && onConflict === UpgradeConflictResolutionEnum.UPGRADE_SOLVABLE
? ruleFieldDiff.conflict !== 'NONE' && ruleFieldDiff.conflict !== 'SOLVABLE'
: ruleFieldDiff.conflict !== 'NONE'
) {
throw new Error(
`Automatic merge calculation for field '${diffableRuleFieldName}' in rule of rule_id ${ruleId} resulted in a conflict. Please resolve the conflict manually or choose another value for 'pick_version'.`
);

View file

@ -10,12 +10,15 @@ import { transformError } from '@kbn/securitysolution-es-utils';
import type {
PerformRuleUpgradeRequestBody,
PerformRuleUpgradeResponseBody,
RuleUpgradeSpecifier,
SkippedRuleUpgrade,
ThreeWayDiff,
} from '../../../../../../common/api/detection_engine/prebuilt_rules';
import {
ModeEnum,
PickVersionValuesEnum,
SkipRuleUpgradeReasonEnum,
ThreeWayDiffConflict,
UpgradeConflictResolutionEnum,
} from '../../../../../../common/api/detection_engine/prebuilt_rules';
import type { SecuritySolutionRequestHandlerContext } from '../../../../../types';
@ -35,6 +38,7 @@ import type {
} from '../../../../../../common/api/detection_engine';
import type { PromisePoolError } from '../../../../../utils/promise_pool';
import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions';
import type { RuleVersions } from '../../logic/diff/calculate_rule_diff';
import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff';
import type { RuleTriad } from '../../model/rule_groups/get_rule_groups';
@ -155,12 +159,18 @@ export const performRuleUpgradeHandler = async (
// Check there's no conflicts
if (onConflict === UpgradeConflictResolutionEnum.SKIP) {
const ruleDiff = calculateRuleDiff(ruleVersions);
const hasConflict = ruleDiff.ruleDiff.num_fields_with_conflicts > 0;
if (hasConflict) {
const ruleUpgradeSpecifier =
request.body.mode === ModeEnum.SPECIFIC_RULES
? request.body.rules.find((x) => x.rule_id === targetRule.rule_id)
: undefined;
const conflict = getRuleUpgradeConflictState(ruleVersions, ruleUpgradeSpecifier);
if (conflict !== ThreeWayDiffConflict.NONE) {
skippedRules.push({
rule_id: targetRule.rule_id,
reason: SkipRuleUpgradeReasonEnum.CONFLICT,
conflict,
});
return;
}
@ -233,3 +243,39 @@ export const performRuleUpgradeHandler = async (
});
}
};
function getRuleUpgradeConflictState(
ruleVersions: RuleVersions,
ruleUpgradeSpecifier?: RuleUpgradeSpecifier
): ThreeWayDiffConflict {
const { ruleDiff } = calculateRuleDiff(ruleVersions);
if (ruleDiff.num_fields_with_conflicts === 0) {
return ThreeWayDiffConflict.NONE;
}
if (!ruleUpgradeSpecifier) {
return ruleDiff.num_fields_with_non_solvable_conflicts > 0
? ThreeWayDiffConflict.NON_SOLVABLE
: ThreeWayDiffConflict.SOLVABLE;
}
let result = ThreeWayDiffConflict.NONE;
// filter out resolved fields
for (const [fieldName, fieldThreeWayDiff] of Object.entries<ThreeWayDiff<unknown>>(
ruleDiff.fields
)) {
const hasResolvedValue =
ruleUpgradeSpecifier.fields?.[fieldName as keyof typeof ruleUpgradeSpecifier.fields]
?.pick_version === 'RESOLVED';
if (fieldThreeWayDiff.conflict === ThreeWayDiffConflict.NON_SOLVABLE && !hasResolvedValue) {
return ThreeWayDiffConflict.NON_SOLVABLE;
} else if (fieldThreeWayDiff.conflict === ThreeWayDiffConflict.SOLVABLE && !hasResolvedValue) {
result = ThreeWayDiffConflict.SOLVABLE;
}
}
return result;
}