mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# 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:
parent
e6f796c6ee
commit
5edf2d1b32
17 changed files with 574 additions and 112 deletions
|
@ -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({
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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> = {};
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -36,7 +36,9 @@ export const UpgradePrebuiltRulesTableButtons = ({
|
|||
|
||||
const doAllSelectedRulesHaveConflicts =
|
||||
isRulesCustomizationEnabled &&
|
||||
selectedRules.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts);
|
||||
selectedRules.every(
|
||||
({ hasNonSolvableUnresolvedConflicts }) => hasNonSolvableUnresolvedConflicts
|
||||
);
|
||||
|
||||
const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({
|
||||
canUserEditRules,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
? [
|
||||
|
|
|
@ -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."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'.`
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue