From 72f2e563bf71b17c77805f5887874e164b002e4b Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 27 Aug 2024 15:14:36 +0200 Subject: [PATCH 01/11] prototype upgrade rules state --- .../upgrade_prebuilt_rules_table_context.tsx | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 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 9d1a3071b44e1..96f9eca2f4198 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 @@ -14,7 +14,10 @@ import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logi import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; import { useBoolState } from '../../../../../common/hooks/use_bool_state'; import { affectedJobIds } from '../../../../../detections/components/callouts/ml_job_compatibility_callout/affected_job_ids'; -import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + PickVersionValues, + RuleUpgradeInfoForReview, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; import { @@ -35,11 +38,27 @@ import { MlJobUpgradeModal } from '../../../../../detections/components/modals/m import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; import * as i18n from './translations'; +type FieldUpgradeState = + | { + pickVersion: PickVersionValues; + } + | { pickVersion: 'RESOLVED'; resolvedValue: unknown }; + +interface RuleUpgradeState { + fields: Record; +} + +type RulesUpgradeState = Record; + export interface UpgradePrebuiltRulesTableState { /** * Rules available to be updated */ rules: RuleUpgradeInfoForReview[]; + /** + * Conflict resolution upgrade state + */ + rulesUpgradeState: RulesUpgradeState; /** * Rules to display in table after applying filters */ @@ -94,6 +113,12 @@ export interface UpgradePrebuiltRulesTableActions { setFilterOptions: Dispatch>; selectRules: (rules: RuleUpgradeInfoForReview[]) => void; openRulePreview: (ruleId: string) => void; + setFieldPickVersion: (params: { + ruleId: string; + fieldName: string; + pickVersion: PickVersionValues; + resolvedValue: unknown; + }) => void; } export interface UpgradePrebuiltRulesContextType { @@ -136,6 +161,28 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change }); + const [rulesUpgradeState, setRulesUpgradeState] = useState({}); + const setFieldPickVersion = useCallback( + (params: { + ruleId: string; + fieldName: string; + pickVersion: PickVersionValues; + resolvedValue: unknown; + }) => { + setRulesUpgradeState((prevRulesUpgradeState) => ({ + ...prevRulesUpgradeState, + [params.ruleId]: { + ...(prevRulesUpgradeState[params.ruleId] ?? {}), + fieldName: { + pickVersion: params.pickVersion, + resolvedValue: params.resolvedValue, + }, + }, + })); + }, + [setRulesUpgradeState] + ); + const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules(); const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); @@ -228,14 +275,23 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setFilterOptions, selectRules: setSelectedRules, openRulePreview, + setFieldPickVersion, }), - [refetch, upgradeOneRule, upgradeSelectedRules, upgradeAllRules, openRulePreview] + [ + refetch, + upgradeOneRule, + upgradeSelectedRules, + upgradeAllRules, + openRulePreview, + setFieldPickVersion, + ] ); const providerValue = useMemo(() => { return { state: { rules, + rulesUpgradeState, filteredRules, filterOptions, tags, @@ -251,6 +307,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ }; }, [ rules, + rulesUpgradeState, filteredRules, filterOptions, tags, From 93e08c26c6f9b514f0d8c856ff6c5d085f89cbfc Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Wed, 28 Aug 2024 16:22:57 +0200 Subject: [PATCH 02/11] use rules upgrade state --- .../diff}/convert_rule_to_diffable.ts | 15 +- .../diff/extract_building_block_object.ts | 18 ++ .../diff}/extract_rule_data_query.ts | 6 +- .../diff}/extract_rule_data_source.ts | 6 +- .../extract_rule_name_override_object.ts | 7 +- .../diff}/extract_rule_schedule.ts | 10 +- .../extract_timeline_template_reference.ts | 7 +- .../extract_timestamp_override_object.ts | 7 +- .../detection_engine/rule_management/utils.ts | 25 ++ .../rule_details/three_way_diff_tab.tsx | 22 ++ .../components/rule_details/translations.ts | 7 + .../upgrade_prebuilt_rules_table_context.tsx | 257 +++++++++--------- .../use_rules_upgrade_state.ts | 52 ++++ .../rules_table/use_rule_preview_flyout.tsx | 68 +++++ .../logic/diff/calculate_rule_diff.ts | 2 +- .../extract_building_block_object.ts | 21 -- ...rt_prebuilt_rule_asset_to_rule_response.ts | 2 +- .../convert_rule_response_to_alerting_rule.ts | 3 +- .../mergers/apply_rule_defaults.ts | 2 +- .../mergers/apply_rule_patch.ts | 2 +- .../rule_source/calculate_is_customized.ts | 2 +- .../rule_management/utils/utils.ts | 20 -- 22 files changed, 349 insertions(+), 212 deletions(-) rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/convert_rule_to_diffable.ts (92%) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_building_block_object.ts rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/extract_rule_data_query.ts (86%) rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/extract_rule_data_source.ts (72%) rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/extract_rule_name_override_object.ts (57%) rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/extract_rule_schedule.ts (85%) rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/extract_timeline_template_reference.ts (59%) rename x-pack/plugins/security_solution/{server/lib/detection_engine/prebuilt_rules/logic/diff/normalization => common/detection_engine/prebuilt_rules/diff}/extract_timestamp_override_object.ts (61%) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/rule_management/utils.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_building_block_object.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts similarity index 92% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts index b0c1cc40462fb..f19d8b41be40b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts @@ -7,8 +7,8 @@ import type { RequiredOptional } from '@kbn/zod-helpers'; import { requiredOptional } from '@kbn/zod-helpers'; -import { DEFAULT_MAX_SIGNALS } from '../../../../../../../common/constants'; -import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { assertUnreachable } from '../../../utility_types'; import type { EqlRule, EqlRuleCreateProps, @@ -27,8 +27,7 @@ import type { ThreatMatchRuleCreateProps, ThresholdRule, ThresholdRuleCreateProps, -} from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; +} from '../../../api/detection_engine/model/rule_schema'; import type { DiffableCommonFields, DiffableCustomQueryFields, @@ -40,7 +39,8 @@ import type { DiffableSavedQueryFields, DiffableThreatMatchFields, DiffableThresholdFields, -} from '../../../../../../../common/api/detection_engine/prebuilt_rules'; +} from '../../../api/detection_engine/prebuilt_rules'; +import { addEcsToRequiredFields } from '../../rule_management/utils'; import { extractBuildingBlockObject } from './extract_building_block_object'; import { extractInlineKqlQuery, @@ -53,13 +53,12 @@ import { extractRuleNameOverrideObject } from './extract_rule_name_override_obje import { extractRuleSchedule } from './extract_rule_schedule'; import { extractTimelineTemplateReference } from './extract_timeline_template_reference'; import { extractTimestampOverrideObject } from './extract_timestamp_override_object'; -import { addEcsToRequiredFields } from '../../../../rule_management/utils/utils'; /** * Normalizes a given rule to the form which is suitable for passing to the diff algorithm. * Read more in the JSDoc description of DiffableRule. */ -export const convertRuleToDiffable = (rule: RuleResponse | PrebuiltRuleAsset): DiffableRule => { +export const convertRuleToDiffable = (rule: RuleResponse): DiffableRule => { const commonFields = extractDiffableCommonFields(rule); switch (rule.type) { @@ -109,7 +108,7 @@ export const convertRuleToDiffable = (rule: RuleResponse | PrebuiltRuleAsset): D }; const extractDiffableCommonFields = ( - rule: RuleResponse | PrebuiltRuleAsset + rule: RuleResponse ): RequiredOptional => { return { // --------------------- REQUIRED FIELDS diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_building_block_object.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_building_block_object.ts new file mode 100644 index 0000000000000..18d6ebfb54ce4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_building_block_object.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema'; +import type { BuildingBlockObject } from '../../../api/detection_engine/prebuilt_rules'; + +export const extractBuildingBlockObject = (rule: RuleResponse): BuildingBlockObject | undefined => { + if (rule.building_block_type == null) { + return undefined; + } + return { + type: rule.building_block_type, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_data_query.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_data_query.ts similarity index 86% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_data_query.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_data_query.ts index 21d3d379f0c77..4f4164c6a0086 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_data_query.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_data_query.ts @@ -11,14 +11,14 @@ import type { KqlQueryLanguage, RuleFilterArray, RuleQuery, -} from '../../../../../../../common/api/detection_engine/model/rule_schema'; +} from '../../../api/detection_engine/model/rule_schema'; import type { InlineKqlQuery, RuleEqlQuery, RuleEsqlQuery, RuleKqlQuery, -} from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import { KqlQueryType } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; +} from '../../../api/detection_engine/prebuilt_rules'; +import { KqlQueryType } from '../../../api/detection_engine/prebuilt_rules'; export const extractRuleKqlQuery = ( query: RuleQuery | undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_data_source.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_data_source.ts similarity index 72% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_data_source.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_data_source.ts index 5ab1562869d26..08407770339e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_data_source.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_data_source.ts @@ -8,9 +8,9 @@ import type { DataViewId, IndexPatternArray, -} from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { RuleDataSource } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import { DataSourceType } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; +} from '../../../api/detection_engine/model/rule_schema'; +import type { RuleDataSource } from '../../../api/detection_engine/prebuilt_rules'; +import { DataSourceType } from '../../../api/detection_engine/prebuilt_rules'; export const extractRuleDataSource = ( indexPatterns: IndexPatternArray | undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_name_override_object.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_name_override_object.ts similarity index 57% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_name_override_object.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_name_override_object.ts index ab7e7fd3c4716..55119608264f9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_name_override_object.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_name_override_object.ts @@ -5,12 +5,11 @@ * 2.0. */ -import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { RuleNameOverrideObject } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; +import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema'; +import type { RuleNameOverrideObject } from '../../../api/detection_engine/prebuilt_rules'; export const extractRuleNameOverrideObject = ( - rule: RuleResponse | PrebuiltRuleAsset + rule: RuleResponse ): RuleNameOverrideObject | undefined => { if (rule.rule_name_override == null) { return undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_schedule.ts similarity index 85% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_schedule.ts index a15a4fcb930cd..6812a0a2f6fc6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_rule_schedule.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_rule_schedule.ts @@ -9,14 +9,10 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; import { parseDuration } from '@kbn/alerting-plugin/common'; -import type { - RuleMetadata, - RuleResponse, -} from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { RuleSchedule } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; +import type { RuleMetadata, RuleResponse } from '../../../api/detection_engine/model/rule_schema'; +import type { RuleSchedule } from '../../../api/detection_engine/prebuilt_rules'; -export const extractRuleSchedule = (rule: RuleResponse | PrebuiltRuleAsset): RuleSchedule => { +export const extractRuleSchedule = (rule: RuleResponse): RuleSchedule => { const interval = rule.interval ?? '5m'; const from = rule.from ?? 'now-6m'; const to = rule.to ?? 'now'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_timeline_template_reference.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_timeline_template_reference.ts similarity index 59% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_timeline_template_reference.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_timeline_template_reference.ts index 03244e5bb0adf..7dcce30061659 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_timeline_template_reference.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_timeline_template_reference.ts @@ -5,12 +5,11 @@ * 2.0. */ -import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { TimelineTemplateReference } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; +import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema'; +import type { TimelineTemplateReference } from '../../../api/detection_engine/prebuilt_rules'; export const extractTimelineTemplateReference = ( - rule: RuleResponse | PrebuiltRuleAsset + rule: RuleResponse ): TimelineTemplateReference | undefined => { if (rule.timeline_id == null) { return undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_timestamp_override_object.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_timestamp_override_object.ts similarity index 61% rename from x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_timestamp_override_object.ts rename to x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_timestamp_override_object.ts index 5b67e4c46bfaf..40f218fe430b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_timestamp_override_object.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/extract_timestamp_override_object.ts @@ -5,12 +5,11 @@ * 2.0. */ -import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { TimestampOverrideObject } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; +import type { RuleResponse } from '../../../api/detection_engine/model/rule_schema'; +import type { TimestampOverrideObject } from '../../../api/detection_engine/prebuilt_rules'; export const extractTimestampOverrideObject = ( - rule: RuleResponse | PrebuiltRuleAsset + rule: RuleResponse ): TimestampOverrideObject | undefined => { if (rule.timestamp_override == null) { return undefined; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/utils.ts new file mode 100644 index 0000000000000..c8e32c471c2c5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/utils.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ecsFieldMap } from '@kbn/alerts-as-data-utils'; +import type { RequiredField, RequiredFieldInput } from '../../api/detection_engine'; + +/* + Computes the boolean "ecs" property value for each required field based on the ECS field map. + "ecs" property indicates whether the required field is an ECS field or not. +*/ +export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): RequiredField[] => + (requiredFields ?? []).map((requiredFieldWithoutEcs) => { + const isEcsField = Boolean( + ecsFieldMap[requiredFieldWithoutEcs.name]?.type === requiredFieldWithoutEcs.type + ); + + return { + ...requiredFieldWithoutEcs, + ecs: isEcsField, + }; + }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx new file mode 100644 index 0000000000000..221905322d3f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { DiffableRule } from '../../../../../common/api/detection_engine'; +import type { SetFieldResolvedValueFn } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state'; + +interface ThreeWayDiffTabProps { + finalDiffableRule: DiffableRule; + setFieldResolvedValue: SetFieldResolvedValueFn; +} + +export function ThreeWayDiffTab({ + finalDiffableRule, + setFieldResolvedValue, +}: ThreeWayDiffTabProps): JSX.Element { + return <>{JSON.stringify(finalDiffableRule)}; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts index 89c22a285e327..f0b96bad1aff5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.ts @@ -28,6 +28,13 @@ export const UPDATES_TAB_LABEL = i18n.translate( } ); +export const DIFF_TAB_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.diffTabLabel', + { + defaultMessage: 'Diff', + } +); + export const JSON_VIEW_UPDATES_TAB_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.jsonViewUpdatesTabLabel', { 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 96f9eca2f4198..c51f3bc312b09 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,17 +8,22 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; -import type { EuiTabbedContentTab } from '@elastic/eui'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { ThreeWayDiffTab } from '../../../../rule_management/components/rule_details/three_way_diff_tab'; import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; import { useBoolState } from '../../../../../common/hooks/use_bool_state'; +import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; import { affectedJobIds } from '../../../../../detections/components/callouts/ml_job_compatibility_callout/affected_job_ids'; import type { - PickVersionValues, + DiffableRule, RuleUpgradeInfoForReview, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { + RuleResponse, + RuleSignatureId, +} from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; import { usePerformUpgradeAllRules, @@ -28,37 +33,19 @@ import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_upgrade'; import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade'; import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; -import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout'; -import { - RuleDetailsFlyout, - TabContentPadding, -} from '../../../../rule_management/components/rule_details/rule_details_flyout'; +import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal'; import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; import * as i18n from './translations'; - -type FieldUpgradeState = - | { - pickVersion: PickVersionValues; - } - | { pickVersion: 'RESOLVED'; resolvedValue: unknown }; - -interface RuleUpgradeState { - fields: Record; -} - -type RulesUpgradeState = Record; +import { useRulesUpgradeState } from './use_rules_upgrade_state'; +import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; export interface UpgradePrebuiltRulesTableState { /** * Rules available to be updated */ rules: RuleUpgradeInfoForReview[]; - /** - * Conflict resolution upgrade state - */ - rulesUpgradeState: RulesUpgradeState; /** * Rules to display in table after applying filters */ @@ -113,12 +100,6 @@ export interface UpgradePrebuiltRulesTableActions { setFilterOptions: Dispatch>; selectRules: (rules: RuleUpgradeInfoForReview[]) => void; openRulePreview: (ruleId: string) => void; - setFieldPickVersion: (params: { - ruleId: string; - fieldName: string; - pickVersion: PickVersionValues; - resolvedValue: unknown; - }) => void; } export interface UpgradePrebuiltRulesContextType { @@ -137,6 +118,9 @@ interface UpgradePrebuiltRulesTableContextProviderProps { export const UpgradePrebuiltRulesTableContextProvider = ({ children, }: UpgradePrebuiltRulesTableContextProviderProps) => { + const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled( + 'prebuiltRulesCustomizationEnabled' + ); const [loadingRules, setLoadingRules] = useState([]); const [selectedRules, setSelectedRules] = useState([]); const [filterOptions, setFilterOptions] = useState({ @@ -161,41 +145,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change }); - const [rulesUpgradeState, setRulesUpgradeState] = useState({}); - const setFieldPickVersion = useCallback( - (params: { - ruleId: string; - fieldName: string; - pickVersion: PickVersionValues; - resolvedValue: unknown; - }) => { - setRulesUpgradeState((prevRulesUpgradeState) => ({ - ...prevRulesUpgradeState, - [params.ruleId]: { - ...(prevRulesUpgradeState[params.ruleId] ?? {}), - fieldName: { - pickVersion: params.pickVersion, - resolvedValue: params.resolvedValue, - }, - }, - })); - }, - [setRulesUpgradeState] - ); + const { rulesUpgradeState, setFieldResolvedValue } = useRulesUpgradeState(); const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules(); const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); - const filteredRules = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules }); - - const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout( - filteredRules.map((upgradeInfo) => upgradeInfo.target_rule) - ); - const canPreviewedRuleBeUpgraded = Boolean( - (previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id)) || - isRefetching || - isUpgradingSecurityPackages - ); + const filteredRuleUpgradeInfos = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules }); // Wrapper to add confirmation modal for users who may be running older ML Jobs that would // be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121 @@ -266,6 +221,111 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ } }, [confirmUpgrade, rules, shouldConfirmUpgrade, upgradeAllRulesRequest]); + const ruleActionsFactory = useCallback( + (rule: RuleResponse, closeRulePreview: () => void) => { + const canPreviewedRuleBeUpgraded = Boolean( + loadingRules.includes(rule.id) || isRefetching || isUpgradingSecurityPackages + ); + + return ( + { + upgradeOneRule(rule.id); + closeRulePreview(); + }} + fill + data-test-subj="updatePrebuiltRuleFromFlyoutButton" + > + {i18n.UPDATE_BUTTON_LABEL} + + ); + }, + [loadingRules, isRefetching, isUpgradingSecurityPackages, upgradeOneRule] + ); + const extraTabsFactory = useCallback( + (rule: RuleResponse) => { + const ruleUpgradeInfo = filteredRuleUpgradeInfos.find(({ id }) => id === rule.id); + + if (!ruleUpgradeInfo) { + return []; + } + + const finalDiffableRule: DiffableRule = { + ...convertRuleToDiffable(ruleUpgradeInfo.target_rule), + ...rulesUpgradeState[rule.id], + }; + + const extraTabs = [ + { + id: 'updates', + name: ( + + <>{ruleDetailsI18n.UPDATES_TAB_LABEL} + + ), + content: ( + + + + ), + }, + { + id: 'jsonViewUpdates', + name: ( + + <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} + + ), + content: ( + + + + ), + }, + ]; + + if (isPrebuiltRulesCustomizationEnabled) { + extraTabs.unshift({ + id: 'diff', + name: ( + + <>{ruleDetailsI18n.DIFF_TAB_LABEL} + + ), + content: ( + + + + ), + }); + } + + return extraTabs; + }, + [ + filteredRuleUpgradeInfos, + rulesUpgradeState, + setFieldResolvedValue, + isPrebuiltRulesCustomizationEnabled, + ] + ); + const filteredRules = useMemo( + () => filteredRuleUpgradeInfos.map((rule) => rule.target_rule), + [filteredRuleUpgradeInfos] + ); + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + rules: filteredRules, + ruleActionsFactory, + extraTabsFactory, + }); + const actions = useMemo( () => ({ reFetchRules: refetch, @@ -275,16 +335,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setFilterOptions, selectRules: setSelectedRules, openRulePreview, - setFieldPickVersion, }), - [ - refetch, - upgradeOneRule, - upgradeSelectedRules, - upgradeAllRules, - openRulePreview, - setFieldPickVersion, - ] + [refetch, upgradeOneRule, upgradeSelectedRules, upgradeAllRules, openRulePreview] ); const providerValue = useMemo(() => { @@ -292,7 +344,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ state: { rules, rulesUpgradeState, - filteredRules, + filteredRules: filteredRuleUpgradeInfos, filterOptions, tags, isFetched, @@ -308,7 +360,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ }, [ rules, rulesUpgradeState, - filteredRules, + filteredRuleUpgradeInfos, filterOptions, tags, isFetched, @@ -322,43 +374,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ actions, ]); - const extraTabs = useMemo(() => { - const activeRule = previewedRule && filteredRules.find(({ id }) => id === previewedRule.id); - - if (!activeRule) { - return []; - } - - return [ - { - id: 'updates', - name: ( - - <>{ruleDetailsI18n.UPDATES_TAB_LABEL} - - ), - content: ( - - - - ), - }, - { - id: 'jsonViewUpdates', - name: ( - - <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} - - ), - content: ( - - - - ), - }, - ]; - }, [previewedRule, filteredRules]); - return ( <> @@ -370,29 +385,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ /> )} {children} - {previewedRule && ( - { - upgradeOneRule(previewedRule.rule_id ?? ''); - closeRulePreview(); - }} - fill - data-test-subj="updatePrebuiltRuleFromFlyoutButton" - > - {i18n.UPDATE_BUTTON_LABEL} - - } - extraTabs={extraTabs} - /> - )} + {rulePreviewFlyout} ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts new file mode 100644 index 0000000000000..7873e5c726bc7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import type { + DiffableAllFields, + RuleObjectId, +} from '../../../../../../common/api/detection_engine'; + +interface RuleUpgradeState { + fields: Partial; +} + +type RulesUpgradeState = Record; + +export type SetFieldResolvedValueFn< + FieldName extends keyof DiffableAllFields = keyof DiffableAllFields +> = (params: { + ruleId: RuleObjectId; + fieldName: FieldName; + resolvedValue: DiffableAllFields[FieldName]; +}) => void; + +interface UseRulesUpgradeStateResult { + rulesUpgradeState: RulesUpgradeState; + setFieldResolvedValue: SetFieldResolvedValueFn; +} + +export function useRulesUpgradeState(): UseRulesUpgradeStateResult { + const [rulesUpgradeState, setRulesUpgradeState] = useState({}); + const setFieldResolvedValue = useCallback( + (...[params]: Parameters) => { + setRulesUpgradeState((prevRulesUpgradeState) => ({ + ...prevRulesUpgradeState, + [params.ruleId]: { + ...(prevRulesUpgradeState[params.ruleId] ?? {}), + fieldName: params.resolvedValue, + }, + })); + }, + [setRulesUpgradeState] + ); + + return { + rulesUpgradeState, + setFieldResolvedValue, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx new file mode 100644 index 0000000000000..5f27b5878d8dd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ReactNode } from 'react'; +import React, { useCallback, useState, useMemo } from 'react'; +import type { EuiTabbedContentTab } from '@elastic/eui'; +import { invariant } from '../../../../../common/utils/invariant'; +import type { RuleObjectId } from '../../../../../common/api/detection_engine'; +import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema'; +import { RuleDetailsFlyout } from '../../../rule_management/components/rule_details/rule_details_flyout'; +import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from './upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; + +interface UseRulePreviewFlyoutParams { + rules: RuleResponse[]; + ruleActionsFactory: (rule: RuleResponse, closeRulePreview: () => void) => ReactNode; + extraTabsFactory?: (rule: RuleResponse) => EuiTabbedContentTab[]; +} + +interface UseRulePreviewFlyoutResult { + rulePreviewFlyout: ReactNode; + openRulePreview: (ruleId: RuleObjectId) => void; + closeRulePreview: () => void; +} + +export function useRulePreviewFlyout({ + rules, + extraTabsFactory, + ruleActionsFactory, +}: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult { + const [rule, setRuleForPreview] = useState(); + const closeRulePreview = useCallback(() => setRuleForPreview(undefined), []); + const ruleActions = useMemo( + () => rule && ruleActionsFactory(rule, closeRulePreview), + [rule, ruleActionsFactory, closeRulePreview] + ); + const extraTabs = useMemo( + () => (rule && extraTabsFactory ? extraTabsFactory(rule) : []), + [rule, extraTabsFactory] + ); + + return { + rulePreviewFlyout: rule && ( + + ), + openRulePreview: useCallback( + (ruleId: RuleObjectId) => { + const ruleToShowInFlyout = rules.find((x) => x.id === ruleId); + + invariant(ruleToShowInFlyout, `Rule with id ${ruleId} not found`); + setRuleForPreview(ruleToShowInFlyout); + }, + [rules, setRuleForPreview] + ), + closeRulePreview, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts index 568c6591a724d..69c2f8a6cd517 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts @@ -18,9 +18,9 @@ import { import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; +import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; import { calculateRuleFieldsDiff } from './calculation/calculate_rule_fields_diff'; -import { convertRuleToDiffable } from './normalization/convert_rule_to_diffable'; export interface RuleVersions { current?: RuleResponse; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_building_block_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_building_block_object.ts deleted file mode 100644 index 4f986464b12f7..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/normalization/extract_building_block_object.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import type { BuildingBlockObject } from '../../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PrebuiltRuleAsset } from '../../../model/rule_assets/prebuilt_rule_asset'; - -export const extractBuildingBlockObject = ( - rule: RuleResponse | PrebuiltRuleAsset -): BuildingBlockObject | undefined => { - if (rule.building_block_type == null) { - return undefined; - } - return { - type: rule.building_block_type, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts index 0cb42100d4512..f7a5d78798880 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response.ts @@ -6,8 +6,8 @@ */ import { v4 as uuidv4 } from 'uuid'; +import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils'; import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; -import { addEcsToRequiredFields } from '../../../utils/utils'; import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules'; import { RULE_DEFAULTS } from '../mergers/apply_rule_defaults'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts index 52a00f1d6f42d..8a2609b712c53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts @@ -9,6 +9,7 @@ import type { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rul import type { ActionsClient } from '@kbn/actions-plugin/server'; import type { RuleActionCamel } from '@kbn/securitysolution-io-ts-alerting-types'; +import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils'; import type { RuleResponse, TypeSpecificCreateProps, @@ -25,7 +26,7 @@ import { assertUnreachable } from '../../../../../../../common/utility_types'; import { convertObjectKeysToCamelCase } from '../../../../../../utils/object_case_converters'; import type { RuleParams, TypeSpecificRuleParams } from '../../../../rule_schema'; import { transformToActionFrequency } from '../../../normalization/rule_actions'; -import { addEcsToRequiredFields, separateActionsAndSystemAction } from '../../../utils/utils'; +import { separateActionsAndSystemAction } from '../../../utils/utils'; /** * These are the fields that are added to the rule response that are not part of the rule params diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts index 837df0b3b2f1f..0263a60ab44ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts @@ -6,6 +6,7 @@ */ import { v4 as uuidv4 } from 'uuid'; +import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils'; import type { RuleCreateProps, RuleSource, @@ -20,7 +21,6 @@ import { normalizeThresholdObject, } from '../../../../../../../common/detection_engine/utils'; import { assertUnreachable } from '../../../../../../../common/utility_types'; -import { addEcsToRequiredFields } from '../../../utils/utils'; export const RULE_DEFAULTS = { enabled: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts index 9d02cd8dbb9df..a8beef1bf2a0e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts @@ -7,6 +7,7 @@ import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { stringifyZodError } from '@kbn/zod-helpers'; +import { addEcsToRequiredFields } from '../../../../../../../common/detection_engine/rule_management/utils'; import type { EqlRule, EqlRuleResponseFields, @@ -44,7 +45,6 @@ import { } from '../../../../../../../common/detection_engine/utils'; import { assertUnreachable } from '../../../../../../../common/utility_types'; import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client'; -import { addEcsToRequiredFields } from '../../../utils/utils'; import { calculateRuleSource } from './rule_source/calculate_rule_source'; interface ApplyRulePatchProps { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts index 4f9bb4a060f6f..92c7d941dd7f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/rule_source/calculate_is_customized.ts @@ -9,7 +9,7 @@ import type { RuleResponse } from '../../../../../../../../common/api/detection_ import { MissingVersion } from '../../../../../../../../common/api/detection_engine'; import type { PrebuiltRuleAsset } from '../../../../../prebuilt_rules'; import { calculateRuleFieldsDiff } from '../../../../../prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff'; -import { convertRuleToDiffable } from '../../../../../prebuilt_rules/logic/diff/normalization/convert_rule_to_diffable'; +import { convertRuleToDiffable } from '../../../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; import { convertPrebuiltRuleAssetToRuleResponse } from '../../converters/convert_prebuilt_rule_asset_to_rule_response'; export function calculateIsCustomized( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts index e7fe0f48e17b6..447d55382ab61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/utils.ts @@ -9,8 +9,6 @@ import { partition, isEmpty } from 'lodash/fp'; import pMap from 'p-map'; import { v4 as uuidv4 } from 'uuid'; -import { ecsFieldMap } from '@kbn/alerts-as-data-utils'; - import type { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server'; import type { FindResult, PartialRule } from '@kbn/alerting-plugin/server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; @@ -18,8 +16,6 @@ import type { RuleAction } from '@kbn/securitysolution-io-ts-alerting-types'; import type { InvestigationFields, - RequiredField, - RequiredFieldInput, RuleResponse, RuleAction as RuleActionSchema, } from '../../../../../common/api/detection_engine/model/rule_schema'; @@ -375,22 +371,6 @@ export const migrateLegacyInvestigationFields = ( return investigationFields; }; -/* - Computes the boolean "ecs" property value for each required field based on the ECS field map. - "ecs" property indicates whether the required field is an ECS field or not. -*/ -export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): RequiredField[] => - (requiredFields ?? []).map((requiredFieldWithoutEcs) => { - const isEcsField = Boolean( - ecsFieldMap[requiredFieldWithoutEcs.name]?.type === requiredFieldWithoutEcs.type - ); - - return { - ...requiredFieldWithoutEcs, - ecs: isEcsField, - }; - }); - export const separateActionsAndSystemAction = ( actionsClient: ActionsClient, actions: RuleActionSchema[] | undefined From ef22e3b513bf0c451097a76a91abd3399f2faaf6 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 29 Aug 2024 13:36:52 +0200 Subject: [PATCH 03/11] adapt useRulePreviewFlyout for AddPrebuiltRulesTableContext --- .../rule_details/use_rule_details_flyout.tsx | 47 ------------- .../add_prebuilt_rules_table_context.tsx | 69 ++++++++++--------- 2 files changed, 38 insertions(+), 78 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/use_rule_details_flyout.tsx diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/use_rule_details_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/use_rule_details_flyout.tsx deleted file mode 100644 index ccce517db5b55..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/use_rule_details_flyout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback } from 'react'; -import { invariant } from '../../../../../common/utils/invariant'; -import type { RuleObjectId } from '../../../../../common/api/detection_engine'; -import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema'; - -export interface RuleDetailsFlyoutState { - previewedRule: RuleResponse | null; -} - -export interface RuleDetailsFlyoutActions { - openRulePreview: (ruleId: RuleObjectId) => void; - closeRulePreview: () => void; -} - -export const useRuleDetailsFlyout = ( - rules: RuleResponse[] -): RuleDetailsFlyoutState & RuleDetailsFlyoutActions => { - const [previewedRule, setRuleForPreview] = React.useState(null); - - const openRulePreview = useCallback( - (ruleId: RuleObjectId) => { - const ruleToShowInFlyout = rules.find((rule) => { - return rule.id === ruleId; - }); - invariant(ruleToShowInFlyout, `Rule with id ${ruleId} not found`); - setRuleForPreview(ruleToShowInFlyout); - }, - [rules, setRuleForPreview] - ); - - const closeRulePreview = useCallback(() => { - setRuleForPreview(null); - }, []); - - return { - openRulePreview, - closeRulePreview, - previewedRule, - }; -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index ac89ff017c78a..1be9a7068f5f3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -20,9 +20,8 @@ import { import { usePrebuiltRulesInstallReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review'; import type { AddPrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_install'; import { useFilterPrebuiltRulesToInstall } from './use_filter_prebuilt_rules_to_install'; -import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout'; +import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; -import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout'; import * as i18n from './translations'; import { isUpgradeReviewRequestEnabled } from './add_prebuilt_rules_utils'; @@ -138,15 +137,6 @@ export const AddPrebuiltRulesTableContextProvider = ({ const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules }); - const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout(filteredRules); - - const isPreviewRuleLoading = - previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id); - const canPreviewedRuleBeInstalled = - !userInfoLoading && - canUserCRUD && - !(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages); - const installOneRule = useCallback( async (ruleId: RuleSignatureId) => { const rule = rules.find((r) => r.rule_id === ruleId); @@ -187,6 +177,42 @@ export const AddPrebuiltRulesTableContextProvider = ({ } }, [installAllRulesRequest, rules]); + const ruleActionsFactory = useCallback( + (rule: RuleResponse, closeRulePreview: () => void) => { + const isPreviewRuleLoading = loadingRules.includes(rule.rule_id); + const canPreviewedRuleBeInstalled = + !userInfoLoading && + canUserCRUD && + !(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages); + + return ( + { + installOneRule(rule.rule_id); + closeRulePreview(); + }} + fill + data-test-subj="installPrebuiltRuleFromFlyoutButton" + > + {i18n.INSTALL_BUTTON_LABEL} + + ); + }, + [ + loadingRules, + userInfoLoading, + canUserCRUD, + isRefetching, + isUpgradingSecurityPackages, + installOneRule, + ] + ); + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ + rules: filteredRules, + ruleActionsFactory, + }); + const actions = useMemo( () => ({ setFilterOptions, @@ -236,26 +262,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ <> {children} - {previewedRule && ( - { - installOneRule(previewedRule.rule_id ?? ''); - closeRulePreview(); - }} - fill - data-test-subj="installPrebuiltRuleFromFlyoutButton" - > - {i18n.INSTALL_BUTTON_LABEL} - - } - /> - )} + {rulePreviewFlyout} ); From c705b561bbdda3f66724f1dbbb93dbdec433b8d5 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 29 Aug 2024 17:28:47 +0200 Subject: [PATCH 04/11] properly convert rule to diffable --- .../logic/diff/calculate_rule_diff.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts index 69c2f8a6cd517..ed4f142d20596 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculate_rule_diff.ts @@ -19,6 +19,7 @@ import type { RuleResponse } from '../../../../../../common/api/detection_engine import { invariant } from '../../../../../../common/utils/invariant'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import { calculateRuleFieldsDiff } from './calculation/calculate_rule_fields_diff'; @@ -65,13 +66,19 @@ export const calculateRuleDiff = (args: RuleVersions): CalculateRuleDiffResult = const { base, current, target } = args; invariant(current != null, 'current version is required'); - const diffableCurrentVersion = convertRuleToDiffable(current); + const diffableCurrentVersion = convertRuleToDiffable( + convertPrebuiltRuleAssetToRuleResponse(current) + ); invariant(target != null, 'target version is required'); - const diffableTargetVersion = convertRuleToDiffable(target); + const diffableTargetVersion = convertRuleToDiffable( + convertPrebuiltRuleAssetToRuleResponse(target) + ); // Base version is optional - const diffableBaseVersion = base ? convertRuleToDiffable(base) : undefined; + const diffableBaseVersion = base + ? convertRuleToDiffable(convertPrebuiltRuleAssetToRuleResponse(base)) + : undefined; const fieldsDiff = calculateRuleFieldsDiff({ base_version: diffableBaseVersion || MissingVersion, From 04f845eb5160df52eeeec084d2b8ab656517c5ad Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Fri, 30 Aug 2024 10:01:21 +0200 Subject: [PATCH 05/11] simplify context implementation --- .../rule_details/three_way_diff_tab.tsx | 2 +- .../add_prebuilt_rules_header_buttons.tsx | 11 +- .../add_prebuilt_rules_table.tsx | 9 +- .../add_prebuilt_rules_table_context.tsx | 14 +- .../upgrade_prebuilt_rules_table.tsx | 28 +-- .../upgrade_prebuilt_rules_table_buttons.tsx | 22 ++- .../upgrade_prebuilt_rules_table_context.tsx | 177 +++++++----------- .../use_prebuilt_rules_upgrade_state.ts | 116 ++++++++++++ .../use_rules_upgrade_state.ts | 52 ----- ...e_upgrade_prebuilt_rules_table_columns.tsx | 44 +++-- 10 files changed, 252 insertions(+), 223 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx index 221905322d3f6..5117fa2d7b93b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff_tab.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { DiffableRule } from '../../../../../common/api/detection_engine'; -import type { SetFieldResolvedValueFn } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state'; +import type { SetFieldResolvedValueFn } from '../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state'; interface ThreeWayDiffTabProps { finalDiffableRule: DiffableRule; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx index dbf9cf298e7ff..b943022f5d53d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx @@ -13,13 +13,18 @@ import * as i18n from './translations'; export const AddPrebuiltRulesHeaderButtons = () => { const { - state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages }, + state: { + selectedRules, + loadingRules, + isRefetching, + isUpgradingSecurityPackages, + hasRulesToInstall, + }, actions: { installAllRules, installSelectedRules }, } = useAddPrebuiltRulesTableContext(); const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData(); const canUserEditRules = canUserCRUD && !isUserDataLoading; - const isRulesAvailableForInstall = rules.length > 0; const numberOfSelectedRules = selectedRules.length ?? 0; const shouldDisplayInstallSelectedRulesButton = numberOfSelectedRules > 0; @@ -46,7 +51,7 @@ export const AddPrebuiltRulesHeaderButtons = () => { iconType="plusInCircle" data-test-subj="installAllRulesButton" onClick={installAllRules} - disabled={!canUserEditRules || !isRulesAvailableForInstall || isRequestInProgress} + disabled={!canUserEditRules || !hasRulesToInstall || isRequestInProgress} aria-label={i18n.INSTALL_ALL_ARIA_LABEL} > {i18n.INSTALL_ALL} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx index 22794ab525dca..86fe6e4f3ede8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table.tsx @@ -32,8 +32,7 @@ export const AddPrebuiltRulesTable = React.memo(() => { const { state: { rules, - filteredRules, - isFetched, + hasRulesToInstall, isLoading, isRefetching, selectedRules, @@ -43,8 +42,6 @@ export const AddPrebuiltRulesTable = React.memo(() => { } = addRulesTableContext; const rulesColumns = useAddPrebuiltRulesTableColumns(); - const isTableEmpty = isFetched && rules.length === 0; - const shouldShowProgress = isUpgradingSecurityPackages || isRefetching; return ( @@ -66,7 +63,7 @@ export const AddPrebuiltRulesTable = React.memo(() => { } loadedContent={ - isTableEmpty ? ( + !hasRulesToInstall ? ( ) : ( <> @@ -80,7 +77,7 @@ export const AddPrebuiltRulesTable = React.memo(() => { (() => { return { state: { - rules, - filteredRules, + rules: filteredRules, filterOptions, tags, + hasRulesToInstall: isFetched && rules.length > 0, isFetched, isLoading, loadingRules, 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.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx index 09b231403b10a..16ba012313f34 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx @@ -15,7 +15,7 @@ import { EuiSkeletonText, EuiSkeletonTitle, } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo, useState } from 'react'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; import { RulesChangelogLink } from '../rules_changelog_link'; @@ -23,6 +23,7 @@ import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters'; import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns'; +import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state'; const NO_ITEMS_MESSAGE = ( { const upgradeRulesTableContext = useUpgradePrebuiltRulesTableContext(); + const [selected, setSelected] = useState([]); const { state: { - rules, - filteredRules, - isFetched, + rulesUpgradeState, + hasRulesToUpgrade, isLoading, - selectedRules, isRefetching, isUpgradingSecurityPackages, }, - actions: { selectRules }, } = upgradeRulesTableContext; + const ruleUpgradeStatesArray = useMemo( + () => Object.values(rulesUpgradeState), + [rulesUpgradeState] + ); const rulesColumns = useUpgradePrebuiltRulesTableColumns(); - - const isTableEmpty = isFetched && rules.length === 0; - const shouldShowProgress = isUpgradingSecurityPackages || isRefetching; return ( @@ -76,7 +76,7 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { } loadedContent={ - isTableEmpty ? ( + !hasRulesToUpgrade ? ( NO_ITEMS_MESSAGE ) : ( <> @@ -95,14 +95,14 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { - + { }} selection={{ selectable: () => true, - onSelectionChange: selectRules, - initialSelected: selectedRules, + onSelectionChange: setSelected, + initialSelected: selected, }} itemId="rule_id" data-test-subj="rules-upgrades-table" 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_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx index 6d4e0641b8bbc..9957520811f79 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx @@ -6,25 +6,35 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import { useUserData } from '../../../../../detections/components/user_info'; import * as i18n from './translations'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; +import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state'; -export const UpgradePrebuiltRulesTableButtons = () => { +interface UpgradePrebuiltRulesTableButtonsProps { + selectedRules: RuleUpgradeState[]; +} + +export const UpgradePrebuiltRulesTableButtons = ({ + selectedRules, +}: UpgradePrebuiltRulesTableButtonsProps) => { const { - state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages }, - actions: { upgradeAllRules, upgradeSelectedRules }, + state: { hasRulesToUpgrade, loadingRules, isRefetching, isUpgradingSecurityPackages }, + actions: { upgradeAllRules, upgradeRules }, } = useUpgradePrebuiltRulesTableContext(); const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData(); const canUserEditRules = canUserCRUD && !isUserDataLoading; - const isRulesAvailableForUpgrade = rules.length > 0; const numberOfSelectedRules = selectedRules.length ?? 0; const shouldDisplayUpgradeSelectedRulesButton = numberOfSelectedRules > 0; const isRuleUpgrading = loadingRules.length > 0; const isRequestInProgress = isRuleUpgrading || isRefetching || isUpgradingSecurityPackages; + const upgradeSelectedRules = useCallback( + () => upgradeRules(selectedRules.map((rule) => rule.rule_id)), + [selectedRules, upgradeRules] + ); return ( @@ -47,7 +57,7 @@ export const UpgradePrebuiltRulesTableButtons = () => { fill iconType="plusInCircle" onClick={upgradeAllRules} - disabled={!canUserEditRules || !isRulesAvailableForUpgrade || isRequestInProgress} + disabled={!canUserEditRules || !hasRulesToUpgrade || isRequestInProgress} data-test-subj="upgradeAllRulesButton" > {i18n.UPDATE_ALL} 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 c51f3bc312b09..362cf96e89f12 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 @@ -14,12 +14,7 @@ import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs'; import { useBoolState } from '../../../../../common/hooks/use_bool_state'; -import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; import { affectedJobIds } from '../../../../../detections/components/callouts/ml_job_compatibility_callout/affected_job_ids'; -import type { - DiffableRule, - RuleUpgradeInfoForReview, -} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { RuleResponse, RuleSignatureId, @@ -38,18 +33,15 @@ import { RuleDiffTab } from '../../../../rule_management/components/rule_details import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal'; import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; import * as i18n from './translations'; -import { useRulesUpgradeState } from './use_rules_upgrade_state'; +import type { RulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; +import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; export interface UpgradePrebuiltRulesTableState { /** - * Rules available to be updated - */ - rules: RuleUpgradeInfoForReview[]; - /** - * Rules to display in table after applying filters + * Rule upgrade state after applying `filterOptions` */ - filteredRules: RuleUpgradeInfoForReview[]; + rulesUpgradeState: RulesUpgradeState; /** * Currently selected table filter */ @@ -58,6 +50,10 @@ export interface UpgradePrebuiltRulesTableState { * All unique tags for all rules */ tags: string[]; + /** + * Indicates whether there are rules (without filters applied) to upgrade. + */ + hasRulesToUpgrade: boolean; /** * Is true then there is no cached data and the query is currently fetching. */ @@ -84,21 +80,15 @@ export interface UpgradePrebuiltRulesTableState { * The timestamp for when the rules were successfully fetched */ lastUpdated: number; - /** - * Rule rows selected in EUI InMemory Table - */ - selectedRules: RuleUpgradeInfoForReview[]; } export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; export interface UpgradePrebuiltRulesTableActions { reFetchRules: () => void; - upgradeOneRule: (ruleId: string) => void; - upgradeSelectedRules: () => void; + upgradeRules: (ruleIds: RuleSignatureId[]) => void; upgradeAllRules: () => void; setFilterOptions: Dispatch>; - selectRules: (rules: RuleUpgradeInfoForReview[]) => void; openRulePreview: (ruleId: string) => void; } @@ -122,7 +112,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ 'prebuiltRulesCustomizationEnabled' ); const [loadingRules, setLoadingRules] = useState([]); - const [selectedRules, setSelectedRules] = useState([]); const [filterOptions, setFilterOptions] = useState({ filter: '', tags: [], @@ -131,7 +120,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); const { - data: { rules, stats: { tags } } = { + data: { rules: ruleUpgradeInfos, stats: { tags } } = { rules: [], stats: { tags: [] }, }, @@ -144,13 +133,12 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ refetchInterval: false, // Disable automatic refetching since request is expensive keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change }); - - const { rulesUpgradeState, setFieldResolvedValue } = useRulesUpgradeState(); - - const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules(); - const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); - - const filteredRuleUpgradeInfos = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules }); + const filteredRuleUpgradeInfos = useFilterPrebuiltRulesToUpgrade({ + filterOptions, + rules: ruleUpgradeInfos, + }); + const { rulesUpgradeState, setFieldResolvedValue } = + usePrebuiltRulesUpgradeState(filteredRuleUpgradeInfos); // Wrapper to add confirmation modal for users who may be running older ML Jobs that would // be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121 @@ -165,51 +153,36 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const shouldConfirmUpgrade = legacyJobsInstalled.length > 0; - const upgradeOneRule = useCallback( - async (ruleId: RuleSignatureId) => { - const rule = rules.find((r) => r.rule_id === ruleId); - invariant(rule, `Rule with id ${ruleId} not found`); + const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules(); + const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); - setLoadingRules((prev) => [...prev, ruleId]); + 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)]); try { if (shouldConfirmUpgrade && !(await confirmUpgrade())) { return; } - await upgradeSpecificRulesRequest([ - { - rule_id: ruleId, - version: rule.diff.fields.version?.target_version ?? rule.current_rule.version, - revision: rule.revision, - }, - ]); + await upgradeSpecificRulesRequest(rulesToUpgrade); } finally { - setLoadingRules((prev) => prev.filter((id) => id !== ruleId)); + setLoadingRules((prev) => + prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)) + ); } }, - [confirmUpgrade, rules, shouldConfirmUpgrade, upgradeSpecificRulesRequest] + [confirmUpgrade, shouldConfirmUpgrade, rulesUpgradeState, upgradeSpecificRulesRequest] ); - const upgradeSelectedRules = useCallback(async () => { - const rulesToUpgrade = selectedRules.map((rule) => ({ - rule_id: rule.rule_id, - version: rule.diff.fields.version?.target_version ?? rule.current_rule.version, - revision: rule.revision, - })); - setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); - try { - if (shouldConfirmUpgrade && !(await confirmUpgrade())) { - return; - } - await upgradeSpecificRulesRequest(rulesToUpgrade); - } finally { - setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id))); - setSelectedRules([]); - } - }, [confirmUpgrade, selectedRules, shouldConfirmUpgrade, upgradeSpecificRulesRequest]); - const upgradeAllRules = useCallback(async () => { // Unselect all rules so that the table doesn't show the "bulk actions" bar - setLoadingRules((prev) => [...prev, ...rules.map((r) => r.rule_id)]); + setLoadingRules((prev) => [...prev, ...ruleUpgradeInfos.map((r) => r.rule_id)]); try { if (shouldConfirmUpgrade && !(await confirmUpgrade())) { return; @@ -217,45 +190,38 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ await upgradeAllRulesRequest(); } finally { setLoadingRules([]); - setSelectedRules([]); } - }, [confirmUpgrade, rules, shouldConfirmUpgrade, upgradeAllRulesRequest]); + }, [confirmUpgrade, ruleUpgradeInfos, shouldConfirmUpgrade, upgradeAllRulesRequest]); const ruleActionsFactory = useCallback( - (rule: RuleResponse, closeRulePreview: () => void) => { - const canPreviewedRuleBeUpgraded = Boolean( - loadingRules.includes(rule.id) || isRefetching || isUpgradingSecurityPackages - ); - - return ( - { - upgradeOneRule(rule.id); - closeRulePreview(); - }} - fill - data-test-subj="updatePrebuiltRuleFromFlyoutButton" - > - {i18n.UPDATE_BUTTON_LABEL} - - ); - }, - [loadingRules, isRefetching, isUpgradingSecurityPackages, upgradeOneRule] + (rule: RuleResponse, closeRulePreview: () => void) => ( + { + upgradeRules([rule.rule_id]); + closeRulePreview(); + }} + fill + data-test-subj="updatePrebuiltRuleFromFlyoutButton" + > + {i18n.UPDATE_BUTTON_LABEL} + + ), + [rulesUpgradeState, loadingRules, isRefetching, isUpgradingSecurityPackages, upgradeRules] ); const extraTabsFactory = useCallback( (rule: RuleResponse) => { - const ruleUpgradeInfo = filteredRuleUpgradeInfos.find(({ id }) => id === rule.id); + const ruleUpgradeState = rulesUpgradeState[rule.rule_id]; - if (!ruleUpgradeInfo) { + if (!ruleUpgradeState) { return []; } - const finalDiffableRule: DiffableRule = { - ...convertRuleToDiffable(ruleUpgradeInfo.target_rule), - ...rulesUpgradeState[rule.id], - }; - const extraTabs = [ { id: 'updates', @@ -266,7 +232,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ ), content: ( - + ), }, @@ -280,8 +246,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ content: ( ), @@ -299,7 +265,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ content: ( @@ -309,12 +275,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return extraTabs; }, - [ - filteredRuleUpgradeInfos, - rulesUpgradeState, - setFieldResolvedValue, - isPrebuiltRulesCustomizationEnabled, - ] + [rulesUpgradeState, setFieldResolvedValue, isPrebuiltRulesCustomizationEnabled] ); const filteredRules = useMemo( () => filteredRuleUpgradeInfos.map((rule) => rule.target_rule), @@ -329,38 +290,33 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const actions = useMemo( () => ({ reFetchRules: refetch, - upgradeOneRule, - upgradeSelectedRules, + upgradeRules, upgradeAllRules, setFilterOptions, - selectRules: setSelectedRules, openRulePreview, }), - [refetch, upgradeOneRule, upgradeSelectedRules, upgradeAllRules, openRulePreview] + [refetch, upgradeRules, upgradeAllRules, openRulePreview] ); const providerValue = useMemo(() => { return { state: { - rules, rulesUpgradeState, - filteredRules: filteredRuleUpgradeInfos, + hasRulesToUpgrade: isFetched && ruleUpgradeInfos.length > 0, filterOptions, tags, isFetched, - isLoading: isLoading && loadingJobs, + isLoading: isLoading || loadingJobs, isRefetching, isUpgradingSecurityPackages, - selectedRules, loadingRules, lastUpdated: dataUpdatedAt, }, actions, }; }, [ - rules, + ruleUpgradeInfos, rulesUpgradeState, - filteredRuleUpgradeInfos, filterOptions, tags, isFetched, @@ -368,7 +324,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ loadingJobs, isRefetching, isUpgradingSecurityPackages, - selectedRules, loadingRules, dataUpdatedAt, actions, 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 new file mode 100644 index 0000000000000..f7d7339a1744e --- /dev/null +++ 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 @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useMemo, useState } from 'react'; +import { + ThreeWayDiffConflict, + type DiffableAllFields, + type DiffableRule, + type RuleObjectId, + type RuleSignatureId, + type RuleUpgradeInfoForReview, +} from '../../../../../../common/api/detection_engine'; +import { convertRuleToDiffable } from '../../../../../../common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable'; + +export interface RuleUpgradeState extends RuleUpgradeInfoForReview { + /** + * Rule containing desired values users expect to see in the upgraded rule. + */ + finalRule: DiffableRule; + /** + * Indicates whether there are conflicts blocking rule upgrading. + */ + hasUnresolvedConflicts: boolean; +} +export type RulesUpgradeState = Record; +export type SetFieldResolvedValueFn< + FieldName extends keyof DiffableAllFields = keyof DiffableAllFields +> = (params: { + ruleId: RuleObjectId; + fieldName: FieldName; + resolvedValue: DiffableAllFields[FieldName]; +}) => void; + +type RuleResolvedConflicts = Partial; +type RulesResolvedConflicts = Record; + +interface UseRulesUpgradeStateResult { + rulesUpgradeState: RulesUpgradeState; + setFieldResolvedValue: SetFieldResolvedValueFn; +} + +export function usePrebuiltRulesUpgradeState( + ruleUpgradeInfos: RuleUpgradeInfoForReview[] +): UseRulesUpgradeStateResult { + const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState({}); + const setFieldResolvedValue = useCallback((...[params]: Parameters) => { + setRulesResolvedConflicts((prevRulesResolvedConflicts) => ({ + ...prevRulesResolvedConflicts, + [params.ruleId]: { + ...(prevRulesResolvedConflicts[params.ruleId] ?? {}), + [params.fieldName]: params.resolvedValue, + }, + })); + }, []); + const rulesUpgradeState = useMemo(() => { + const state: RulesUpgradeState = {}; + + for (const ruleUpgradeInfo of ruleUpgradeInfos) { + state[ruleUpgradeInfo.rule_id] = { + ...ruleUpgradeInfo, + finalRule: calcFinalDiffableRule( + ruleUpgradeInfo, + rulesResolvedConflicts[ruleUpgradeInfo.id] ?? {} + ), + hasUnresolvedConflicts: + calcUnresolvedConflicts( + ruleUpgradeInfo, + rulesResolvedConflicts[ruleUpgradeInfo.id] ?? {} + ) > 0, + }; + } + + return state; + }, [ruleUpgradeInfos, rulesResolvedConflicts]); + + return { + rulesUpgradeState, + setFieldResolvedValue, + }; +} + +function calcFinalDiffableRule( + ruleUpgradeInfo: RuleUpgradeInfoForReview, + ruleResolvedConflicts: RuleResolvedConflicts +): DiffableRule { + return { + ...convertRuleToDiffable(ruleUpgradeInfo.target_rule), + ...ruleResolvedConflicts, + } as DiffableRule; +} + +function calcUnresolvedConflicts( + ruleUpgradeInfo: RuleUpgradeInfoForReview, + ruleResolvedConflicts: RuleResolvedConflicts +): number { + const fieldNames = Object.keys(ruleUpgradeInfo.diff.fields) as Array< + keyof typeof ruleUpgradeInfo.diff.fields + >; + const fieldNamesWithNonSolvableConflicts = fieldNames.filter( + (fieldName) => + ruleUpgradeInfo.diff.fields[fieldName]?.conflict === ThreeWayDiffConflict.NON_SOLVABLE + ); + const fieldsWithConflicts = new Set(fieldNamesWithNonSolvableConflicts); + + for (const resolvedConflictField of Object.keys(ruleResolvedConflicts)) { + if (fieldsWithConflicts.has(resolvedConflictField)) { + fieldsWithConflicts.delete(resolvedConflictField); + } + } + + return fieldsWithConflicts.size; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts deleted file mode 100644 index 7873e5c726bc7..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_rules_upgrade_state.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useState } from 'react'; -import type { - DiffableAllFields, - RuleObjectId, -} from '../../../../../../common/api/detection_engine'; - -interface RuleUpgradeState { - fields: Partial; -} - -type RulesUpgradeState = Record; - -export type SetFieldResolvedValueFn< - FieldName extends keyof DiffableAllFields = keyof DiffableAllFields -> = (params: { - ruleId: RuleObjectId; - fieldName: FieldName; - resolvedValue: DiffableAllFields[FieldName]; -}) => void; - -interface UseRulesUpgradeStateResult { - rulesUpgradeState: RulesUpgradeState; - setFieldResolvedValue: SetFieldResolvedValueFn; -} - -export function useRulesUpgradeState(): UseRulesUpgradeStateResult { - const [rulesUpgradeState, setRulesUpgradeState] = useState({}); - const setFieldResolvedValue = useCallback( - (...[params]: Parameters) => { - setRulesUpgradeState((prevRulesUpgradeState) => ({ - ...prevRulesUpgradeState, - [params.ruleId]: { - ...(prevRulesUpgradeState[params.ruleId] ?? {}), - fieldName: params.resolvedValue, - }, - })); - }, - [setRulesUpgradeState] - ); - - return { - rulesUpgradeState, - setFieldResolvedValue, - }; -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx index b188b1afea7f2..5552145de7b68 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_upgrade_prebuilt_rules_table_columns.tsx @@ -10,7 +10,6 @@ import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@ import React, { useMemo } from 'react'; import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name'; import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; -import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { PopoverItems } from '../../../../../common/components/popover_items'; import { useUiSetting$ } from '../../../../../common/lib/kibana'; @@ -23,8 +22,9 @@ import type { Rule } from '../../../../rule_management/logic'; import { getNormalizedSeverity } from '../helpers'; import type { UpgradePrebuiltRulesTableActions } from './upgrade_prebuilt_rules_table_context'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; +import type { RuleUpgradeState } from './use_prebuilt_rules_upgrade_state'; -export type TableColumn = EuiBasicTableColumn; +export type TableColumn = EuiBasicTableColumn; interface RuleNameProps { name: string; @@ -51,10 +51,9 @@ const RuleName = ({ name, ruleId }: RuleNameProps) => { const RULE_NAME_COLUMN: TableColumn = { field: 'current_rule.name', name: i18n.COLUMN_RULE, - render: ( - value: RuleUpgradeInfoForReview['current_rule']['name'], - rule: RuleUpgradeInfoForReview - ) => , + render: (value: RuleUpgradeState['current_rule']['name'], ruleUpgradeState: RuleUpgradeState) => ( + + ), sortable: true, truncateText: true, width: '60%', @@ -106,30 +105,30 @@ const INTEGRATIONS_COLUMN: TableColumn = { }; const createUpgradeButtonColumn = ( - upgradeOneRule: UpgradePrebuiltRulesTableActions['upgradeOneRule'], + upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'], loadingRules: RuleSignatureId[], isDisabled: boolean ): TableColumn => ({ field: 'rule_id', name: , - render: (ruleId: RuleUpgradeInfoForReview['rule_id']) => { + render: (ruleId: RuleSignatureId, record) => { const isRuleUpgrading = loadingRules.includes(ruleId); - const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled; + const isUpgradeButtonDisabled = isRuleUpgrading || isDisabled || record.hasUnresolvedConflicts; + const spinner = ( + + ); + return ( upgradeOneRule(ruleId)} + onClick={() => upgradeRules([ruleId])} data-test-subj={`upgradeSinglePrebuiltRuleButton-${ruleId}`} > - {isRuleUpgrading ? ( - - ) : ( - i18n.UPDATE_RULE_BUTTON - )} + {isRuleUpgrading ? spinner : i18n.UPDATE_RULE_BUTTON} ); }, @@ -143,9 +142,8 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING); const { state: { loadingRules, isRefetching, isUpgradingSecurityPackages }, - actions: { upgradeOneRule }, + actions: { upgradeRules }, } = useUpgradePrebuiltRulesTableContext(); - const isDisabled = isRefetching || isUpgradingSecurityPackages; return useMemo( @@ -169,15 +167,15 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => { field: 'current_rule.severity', name: i18n.COLUMN_SEVERITY, render: (value: Rule['severity']) => , - sortable: ({ current_rule: { severity } }: RuleUpgradeInfoForReview) => + sortable: ({ current_rule: { severity } }: RuleUpgradeState) => getNormalizedSeverity(severity), truncateText: true, width: '12%', }, ...(hasCRUDPermissions - ? [createUpgradeButtonColumn(upgradeOneRule, loadingRules, isDisabled)] + ? [createUpgradeButtonColumn(upgradeRules, loadingRules, isDisabled)] : []), ], - [hasCRUDPermissions, loadingRules, isDisabled, showRelatedIntegrations, upgradeOneRule] + [hasCRUDPermissions, loadingRules, isDisabled, showRelatedIntegrations, upgradeRules] ); }; From 64ccdde406e0b308cbeb06f7057a3b8f089c171d Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Mon, 9 Sep 2024 11:55:38 +0200 Subject: [PATCH 06/11] add rule preview flyout props --- .../add_prebuilt_rules_table_context.tsx | 6 ++++++ .../upgrade_prebuilt_rules_table_context.tsx | 4 ++++ .../rules_table/use_rule_preview_flyout.tsx | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index 30bddc1daba18..52c04dcef76b0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -94,6 +94,8 @@ interface AddPrebuiltRulesTableContextProviderProps { children: React.ReactNode; } +const PREBUILT_RULE_INSTALL_FLYOUT_ANCHOR = 'installPrebuiltRulePreview'; + export const AddPrebuiltRulesTableContextProvider = ({ children, }: AddPrebuiltRulesTableContextProviderProps) => { @@ -211,6 +213,10 @@ export const AddPrebuiltRulesTableContextProvider = ({ const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ rules: filteredRules, ruleActionsFactory, + flyoutProps: { + id: PREBUILT_RULE_INSTALL_FLYOUT_ANCHOR, + dataTestSubj: PREBUILT_RULE_INSTALL_FLYOUT_ANCHOR, + }, }); const actions = useMemo( 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 362cf96e89f12..4fee1cf8f3560 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 @@ -285,6 +285,10 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ rules: filteredRules, ruleActionsFactory, extraTabsFactory, + flyoutProps: { + id: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + dataTestSubj: PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR, + }, }); const actions = useMemo( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx index 5f27b5878d8dd..ccdfd5c39177e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rule_preview_flyout.tsx @@ -12,12 +12,20 @@ import { invariant } from '../../../../../common/utils/invariant'; import type { RuleObjectId } from '../../../../../common/api/detection_engine'; import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema'; import { RuleDetailsFlyout } from '../../../rule_management/components/rule_details/rule_details_flyout'; -import { PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR } from './upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context'; interface UseRulePreviewFlyoutParams { rules: RuleResponse[]; ruleActionsFactory: (rule: RuleResponse, closeRulePreview: () => void) => ReactNode; extraTabsFactory?: (rule: RuleResponse) => EuiTabbedContentTab[]; + flyoutProps: RulePreviewFlyoutProps; +} + +interface RulePreviewFlyoutProps { + /** + * Rule preview flyout unique id used in HTML + */ + id: string; + dataTestSubj: string; } interface UseRulePreviewFlyoutResult { @@ -30,6 +38,7 @@ export function useRulePreviewFlyout({ rules, extraTabsFactory, ruleActionsFactory, + flyoutProps, }: UseRulePreviewFlyoutParams): UseRulePreviewFlyoutResult { const [rule, setRuleForPreview] = useState(); const closeRulePreview = useCallback(() => setRuleForPreview(undefined), []); @@ -47,8 +56,8 @@ export function useRulePreviewFlyout({ Date: Tue, 10 Sep 2024 15:30:23 +0200 Subject: [PATCH 07/11] add a new line for readability --- .../add_prebuilt_rules_table_context.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index 52c04dcef76b0..5450fc1f64a1c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -210,6 +210,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ installOneRule, ] ); + const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ rules: filteredRules, ruleActionsFactory, From 298f486dd1956750a52e32035fcd3000a8b42108 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 10 Sep 2024 15:30:50 +0200 Subject: [PATCH 08/11] add merge values to final diffable rule --- .../use_prebuilt_rules_upgrade_state.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 f7d7339a1744e..6de96c30933e1 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 @@ -6,6 +6,7 @@ */ import { useCallback, useMemo, useState } from 'react'; +import type { FieldsDiff } from '../../../../../../common/api/detection_engine'; import { ThreeWayDiffConflict, type DiffableAllFields, @@ -89,10 +90,26 @@ function calcFinalDiffableRule( ): DiffableRule { return { ...convertRuleToDiffable(ruleUpgradeInfo.target_rule), + ...convertRuleFieldsDiffToDiffable(ruleUpgradeInfo.diff.fields), ...ruleResolvedConflicts, } as DiffableRule; } +/** + * Assembles a `DiffableRule` from rule fields diff `merge_value`s. + */ +function convertRuleFieldsDiffToDiffable( + ruleFieldsDiff: FieldsDiff> +): Partial { + const mergeVersionRule: Record = {}; + + for (const fieldName of Object.keys(ruleFieldsDiff)) { + mergeVersionRule[fieldName] = ruleFieldsDiff[fieldName].merged_version; + } + + return mergeVersionRule; +} + function calcUnresolvedConflicts( ruleUpgradeInfo: RuleUpgradeInfoForReview, ruleResolvedConflicts: RuleResolvedConflicts From 6663685d5828c5dacdb5ef907fc63a26e2ab17ce Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 10 Sep 2024 15:40:30 +0200 Subject: [PATCH 09/11] count SOLABLE and NON_SOLVABLE conflicts to lock rule upgrading --- .../use_prebuilt_rules_upgrade_state.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) 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 6de96c30933e1..7342f3e4bdc27 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 @@ -68,8 +68,8 @@ export function usePrebuiltRulesUpgradeState( rulesResolvedConflicts[ruleUpgradeInfo.id] ?? {} ), hasUnresolvedConflicts: - calcUnresolvedConflicts( - ruleUpgradeInfo, + getUnacceptedConflictsCount( + ruleUpgradeInfo.diff.fields, rulesResolvedConflicts[ruleUpgradeInfo.id] ?? {} ) > 0, }; @@ -110,24 +110,21 @@ function convertRuleFieldsDiffToDiffable( return mergeVersionRule; } -function calcUnresolvedConflicts( - ruleUpgradeInfo: RuleUpgradeInfoForReview, +function getUnacceptedConflictsCount( + ruleFieldsDiff: FieldsDiff>, ruleResolvedConflicts: RuleResolvedConflicts ): number { - const fieldNames = Object.keys(ruleUpgradeInfo.diff.fields) as Array< - keyof typeof ruleUpgradeInfo.diff.fields - >; - const fieldNamesWithNonSolvableConflicts = fieldNames.filter( - (fieldName) => - ruleUpgradeInfo.diff.fields[fieldName]?.conflict === ThreeWayDiffConflict.NON_SOLVABLE + const fieldNames = Object.keys(ruleFieldsDiff); + const fieldNamesWithConflict = fieldNames.filter( + (fieldName) => ruleFieldsDiff[fieldName].conflict !== ThreeWayDiffConflict.NONE ); - const fieldsWithConflicts = new Set(fieldNamesWithNonSolvableConflicts); + const fieldNamesWithConflictSet = new Set(fieldNamesWithConflict); for (const resolvedConflictField of Object.keys(ruleResolvedConflicts)) { - if (fieldsWithConflicts.has(resolvedConflictField)) { - fieldsWithConflicts.delete(resolvedConflictField); + if (fieldNamesWithConflictSet.has(resolvedConflictField)) { + fieldNamesWithConflictSet.delete(resolvedConflictField); } } - return fieldsWithConflicts.size; + return fieldNamesWithConflictSet.size; } From 7e61fada1b0d03d1a66a003b519ce4113180647e Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 10 Sep 2024 15:51:40 +0200 Subject: [PATCH 10/11] fix improper id usage --- .../use_prebuilt_rules_upgrade_state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7342f3e4bdc27..86f2293d312fa 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 @@ -65,12 +65,12 @@ export function usePrebuiltRulesUpgradeState( ...ruleUpgradeInfo, finalRule: calcFinalDiffableRule( ruleUpgradeInfo, - rulesResolvedConflicts[ruleUpgradeInfo.id] ?? {} + rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {} ), hasUnresolvedConflicts: getUnacceptedConflictsCount( ruleUpgradeInfo.diff.fields, - rulesResolvedConflicts[ruleUpgradeInfo.id] ?? {} + rulesResolvedConflicts[ruleUpgradeInfo.rule_id] ?? {} ) > 0, }; } From d72f1b4f03f935bd0434f03567627239c9ceb3f9 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Wed, 11 Sep 2024 00:10:59 +0200 Subject: [PATCH 11/11] mock historical prebuilt rule versions --- .../cypress/tasks/api_calls/prebuilt_rules.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts index f4273cad22299..420794de7e338 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts @@ -145,15 +145,24 @@ export const bulkCreateRuleAssets = ({ const url = `${Cypress.env('ELASTICSEARCH_URL')}/${index}/_bulk?refresh`; const bulkIndexRequestBody = rules.reduce((body, rule) => { - const indexOperation = { + const document = JSON.stringify(rule); + const documentId = `security-rule:${rule['security-rule'].rule_id}`; + const historicalDocumentId = `${documentId}_${rule['security-rule'].version}`; + + const indexRuleAsset = `${JSON.stringify({ + index: { + _index: index, + _id: documentId, + }, + })}\n${document}\n`; + const indexHistoricalRuleAsset = `${JSON.stringify({ index: { _index: index, - _id: `security-rule:${rule['security-rule'].rule_id}`, + _id: historicalDocumentId, }, - }; + })}\n${document}\n`; - const documentData = JSON.stringify(rule); - return body.concat(JSON.stringify(indexOperation), '\n', documentData, '\n'); + return body.concat(indexRuleAsset, indexHistoricalRuleAsset); }, ''); rootRequest({