From fbc9050373eafd8665f6f2ad3f740c817ee549cc Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 14 Nov 2024 15:13:20 +0100 Subject: [PATCH] [Security Solution] Integrate Prebuilt Rules Customization UI with the `_perform` upgrade API (#199761) **Partially addresses:** https://github.com/elastic/kibana/issues/171520 ## Summary This PR integrates Prebuilt Rules Customization UI functionality with the `/internal/detection_engine/prebuilt_rules/upgrade/_perform` Prebuilt Rules Customization upgrade API. > [!CAUTION] > This PR doesn't handle rule type changes. Prebuilt rule updates with rule type change consider having a NON SOLVABLE conflict and won't be upgraded neither individually nor in bulk. Addressing that task requires UI and functional changes and will be addressed in a separate PR. ## Details ## How to test - Clear Elasticsearch data - Run Elasticsearch and Kibana locally (do not open Kibana in a web browser) - Install an outdated version of the `security_detection_engine` Fleet package ```bash curl -X POST --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H "elastic-api-version: 2023-10-31" -d '{"force":true}' http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1 ``` - Install prebuilt rules ```bash curl -X POST --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H "elastic-api-version: 1" -d '{"mode":"ALL_RULES"}' http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform ``` - Open `Detection Rules (SIEM)` Page -> `Rule Updates` - [ ] Check update functionality in a flyout - Pick a rule - Click on rule's name - Make changes to fields in incoming rule updates updates - Save field(s) changes - Press the `Update` button - [ ] Check table row rule update - Pick a rule - Click on rule's name - Make changes to fields in incoming rule updates updates - Save field(s) changes - Close the flyout - Press the `Update rule` button in the rule's table row - [ ] Check bulk rule update - Pick a few rules and for each of them do the next steps - Click on rule's name - Make changes to fields in incoming rule updates updates - Save field(s) changes - Close the flyout - After press the `Update All` button on the page - [ ] Check selected rules bulk update - Pick a few rules and for each of them do the next steps - Click on rule's name - Make changes to fields in incoming rule updates updates - Save field(s) changes - Close the flyout - After select the modified rule updates - Press the `Update N selected rule(s)` button on the page Co-authored-by: Dmitrii Shevchenko --- .../upgrade_prebuilt_rules_table_context.tsx | 106 +++++++++++------- .../use_prebuilt_rules_upgrade_state.ts | 3 +- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index cbb0350da173..4217fa111786 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -8,10 +8,18 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { isNonUpgradeableFieldName } from '../../../../rule_management/model/prebuilt_rule_upgrade/fields'; +import type { + RuleFieldsToUpgrade, + RuleUpgradeSpecifier, + RuleUpgradeInfoForReview, +} from '../../../../../../common/api/detection_engine'; import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled'; import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; -import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine'; -import type { RulesUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; +import type { + RuleUpgradeState, + RulesUpgradeState, +} from '../../../../rule_management/model/prebuilt_rule_upgrade'; import { RuleUpgradeConflictsResolverTab } from '../../../../rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab'; import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; @@ -26,13 +34,14 @@ import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebui import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade'; import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; +import { FieldUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/field_upgrade_state'; +import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; import { MlJobUpgradeModal } from './modals/ml_job_upgrade_modal'; import { UpgradeConflictsModal } from './modals/upgrade_conflicts_modal'; -import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; -import * as i18n from './translations'; import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; -import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; import { useMlJobUpgradeModal, useUpgradeConflictsModal } from './use_upgrade_modals'; +import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; +import * as i18n from './translations'; export interface UpgradePrebuiltRulesTableState { /** @@ -160,20 +169,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ } = useUpgradeConflictsModal(); const shouldConfirmMLJobs = legacyJobsInstalled.length > 0; - const getRulesWithConflicts = useCallback( - (ruleIds?: RuleSignatureId[]) => { - const rulesToUpgrade = - ruleIds?.map((ruleId) => { - const rule = rulesUpgradeState[ruleId]; - invariant(rule, `Rule with ID ${ruleId} not found.`); - - return rule; - }) ?? []; - - return rulesToUpgrade.filter((rule) => rule.diff.num_fields_with_conflicts > 0); - }, - [rulesUpgradeState] - ); const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules({ pickVersion: isPrebuiltRulesCustomizationEnabled ? 'MERGED' : 'TARGET', @@ -181,49 +176,57 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const upgradeRules = useCallback( async (ruleIds: RuleSignatureId[]) => { - const rulesToUpgrade = ruleIds.map((ruleId) => ({ - rule_id: ruleId, - version: - rulesUpgradeState[ruleId].diff.fields.version?.target_version ?? - rulesUpgradeState[ruleId].current_rule.version, - revision: rulesUpgradeState[ruleId].revision, - })); - setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); + const conflictRuleIdsSet = new Set( + isPrebuiltRulesCustomizationEnabled + ? ruleIds.filter( + (ruleId) => + rulesUpgradeState[ruleId].diff.num_fields_with_conflicts > 0 && + rulesUpgradeState[ruleId].hasUnresolvedConflicts + ) + : [] + ); + const ruleUpgradeSpecifiers: RuleUpgradeSpecifier[] = ruleIds + .filter((ruleId) => !conflictRuleIdsSet.has(ruleId)) + .map((ruleId) => ({ + rule_id: ruleId, + version: + rulesUpgradeState[ruleId].diff.fields.version?.target_version ?? + rulesUpgradeState[ruleId].current_rule.version, + revision: rulesUpgradeState[ruleId].revision, + fields: isPrebuiltRulesCustomizationEnabled + ? constructRuleFieldsToUpgrade(rulesUpgradeState[ruleId]) + : undefined, + })); + + setLoadingRules((prev) => [...prev, ...ruleUpgradeSpecifiers.map((x) => x.rule_id)]); + try { // Handle MLJobs modal if (shouldConfirmMLJobs && !(await confirmLegacyMLJobs())) { return; } - // Handle Rule Upgrades modal - const rulesWithConflicts = getRulesWithConflicts(ruleIds); if ( isPrebuiltRulesCustomizationEnabled && - rulesWithConflicts.length > 0 && + conflictRuleIdsSet.size > 0 && !(await confirmConflictsUpgrade()) ) { return; } - // Prepare payload for upgrade with rules with no conflicts - const ruleIdsWithConflicts = new Set(rulesWithConflicts.map((rule) => rule.rule_id)); - const rulesToUpgradeWithNoConflicts = isPrebuiltRulesCustomizationEnabled - ? rulesToUpgrade.filter((rule) => !ruleIdsWithConflicts.has(rule.rule_id)) - : rulesToUpgrade; - await upgradeSpecificRulesRequest(rulesToUpgradeWithNoConflicts); + await upgradeSpecificRulesRequest(ruleUpgradeSpecifiers); } catch (err) { addError(err, { title: i18n.UPDATE_ERROR }); } finally { - setLoadingRules((prev) => - prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)) - ); + const upgradedRuleIdsSet = new Set(ruleUpgradeSpecifiers.map((x) => x.rule_id)); + + setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); } }, [ confirmLegacyMLJobs, confirmConflictsUpgrade, shouldConfirmMLJobs, - getRulesWithConflicts, rulesUpgradeState, upgradeSpecificRulesRequest, isPrebuiltRulesCustomizationEnabled, @@ -408,3 +411,26 @@ export const useUpgradePrebuiltRulesTableContext = (): UpgradePrebuiltRulesConte return rulesTableContext; }; + +function constructRuleFieldsToUpgrade(ruleUpgradeState: RuleUpgradeState): RuleFieldsToUpgrade { + const finalRule = ruleUpgradeState.finalRule as Record; + const ruleFieldsToUpgrade: Record = {}; + + for (const fieldName of Object.keys(ruleUpgradeState.fieldsUpgradeState)) { + const fieldUpgradeState = ruleUpgradeState.fieldsUpgradeState[fieldName]; + + if (!isNonUpgradeableFieldName(fieldName) && fieldUpgradeState === FieldUpgradeState.Accepted) { + invariant( + fieldName in finalRule, + `Ready to upgrade field "${fieldName}" is not found in final rule` + ); + + ruleFieldsToUpgrade[fieldName] = { + pick_version: 'RESOLVED', + resolved_value: finalRule[fieldName], + }; + } + } + + return ruleFieldsToUpgrade; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts index 8c97a4ef52e2..b50ca7da1849 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts @@ -19,11 +19,12 @@ import { type DiffableRule, type RuleUpgradeInfoForReview, ThreeWayDiffConflict, + type RuleSignatureId, } from '../../../../../../common/api/detection_engine'; import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; type RuleResolvedConflicts = Partial; -type RulesResolvedConflicts = Record; +type RulesResolvedConflicts = Record; interface UseRulesUpgradeStateResult { rulesUpgradeState: RulesUpgradeState;