From bb96139c86636b59481518ac87dada8a79efcd38 Mon Sep 17 00:00:00 2001
From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
Date: Wed, 6 Nov 2024 14:51:25 -0500
Subject: [PATCH] [Security Solution] Adds UI support for filtering by rule
source customization (#197340)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Addresses https://github.com/elastic/kibana/issues/180169
> [!NOTE]
> Feature is behind the `prebuiltRulesCustomizationEnabled` feature
flag.
Adds a filter for prebuilt rules in the Update rules table for
"Modified" and "Unmodified" rules. Also adds a badge column in the Rules
table to display whether a prebuilt rule has been customized or not.
Also switches the "Customized Elastic rule" badge on the rule details
page to align with the updated language of "_Modified_ Elastic rule"
### Screenshots
#### Modified badge in Rules table
![Screenshot 2024-11-05 at 3 05
56 PM](https://github.com/user-attachments/assets/1f3313bb-7171-42b5-99b0-b9fb296fefd3)
#### Modification filter dropdown on Rule update page
#### New "customized rule" badge language on Rule details page
![Screenshot 2024-11-05 at 3 14
58 PM](https://github.com/user-attachments/assets/4e22ba3a-e13f-4cf1-88c0-6b5b0b2c258a)
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels)
- [ ] This will appear in the **Release Notes** and follow the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: Elastic Machine
---
.../customized_prebuilt_rule_badge.tsx | 8 +-
.../components/rule_details/translations.ts | 4 +-
.../hooks/use_default_index_pattern.tsx | 6 +-
...s_prebuilt_rules_customization_enabled.tsx | 12 ++
.../rule_management/logic/types.ts | 6 +
.../upgrade_prebuilt_rules_table_context.tsx | 7 +-
.../upgrade_prebuilt_rules_table_filters.tsx | 48 ++++++--
...rade_rule_customization_filter_popover.tsx | 92 +++++++++++++++
.../use_filter_prebuilt_rules_to_upgrade.ts | 28 ++++-
.../use_prebuilt_rules_upgrade_state.ts | 6 +-
...e_upgrade_prebuilt_rules_table_columns.tsx | 44 ++++++-
.../components/rules_table/use_columns.tsx | 54 ++++++++-
.../integrations_popover/index.tsx | 7 +-
.../detection_engine/rules/translations.ts | 35 ++++++
...low_with_prebuilt_rule_customization.cy.ts | 110 ++++++++++++++++++
.../cypress/screens/alerts_detection_rules.ts | 2 +
.../cypress/screens/rule_updates.ts | 12 ++
.../cypress/tasks/api_calls/rules.ts | 17 +++
.../cypress/tasks/prebuilt_rules.ts | 10 ++
19 files changed, 470 insertions(+), 38 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx
create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx
create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_with_prebuilt_rule_customization.cy.ts
create mode 100644 x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx
index 56a559a91794a..e4b00196f4768 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/customized_prebuilt_rule_badge.tsx
@@ -10,7 +10,7 @@ import { EuiBadge } from '@elastic/eui';
import * as i18n from './translations';
import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine';
import type { RuleResponse } from '../../../../../common/api/detection_engine';
-import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
+import { useIsPrebuiltRulesCustomizationEnabled } from '../../hooks/use_is_prebuilt_rules_customization_enabled';
interface CustomizedPrebuiltRuleBadgeProps {
rule: RuleResponse | null;
@@ -19,9 +19,7 @@ interface CustomizedPrebuiltRuleBadgeProps {
export const CustomizedPrebuiltRuleBadge: React.FC = ({
rule,
}) => {
- const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled(
- 'prebuiltRulesCustomizationEnabled'
- );
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
if (!isPrebuiltRulesCustomizationEnabled) {
return null;
@@ -31,5 +29,5 @@ export const CustomizedPrebuiltRuleBadge: React.FC{i18n.CUSTOMIZED_PREBUILT_RULE_LABEL};
+ return {i18n.MODIFIED_PREBUILT_RULE_LABEL};
};
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..e7f36e2011f3c 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
@@ -350,10 +350,10 @@ export const MAX_SIGNALS_FIELD_LABEL = i18n.translate(
}
);
-export const CUSTOMIZED_PREBUILT_RULE_LABEL = i18n.translate(
+export const MODIFIED_PREBUILT_RULE_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.customizedPrebuiltRuleLabel',
{
- defaultMessage: 'Customized Elastic rule',
+ defaultMessage: 'Modified Elastic rule',
}
);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx
index b5ca86c6f1f57..5450cf9950d59 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_default_index_pattern.tsx
@@ -7,7 +7,7 @@
import { useKibana } from '../../../common/lib/kibana/kibana_react';
import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
-import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
+import { useIsPrebuiltRulesCustomizationEnabled } from './use_is_prebuilt_rules_customization_enabled';
/**
* Gets the default index pattern for cases when rule has neither index patterns or data view.
@@ -15,9 +15,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_exper
*/
export function useDefaultIndexPattern(): string[] {
const { services } = useKibana();
- const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled(
- 'prebuiltRulesCustomizationEnabled'
- );
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
return isPrebuiltRulesCustomizationEnabled
? services.settings.client.get(DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN)
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx
new file mode 100644
index 0000000000000..d25925860c175
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled.tsx
@@ -0,0 +1,12 @@
+/*
+ * 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 { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
+
+export const useIsPrebuiltRulesCustomizationEnabled = () => {
+ return useIsExperimentalFeatureEnabled('prebuiltRulesCustomizationEnabled');
+};
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts
index e12442c97aa4c..59ac52d592bcd 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts
@@ -99,6 +99,7 @@ export interface FilterOptions {
excludeRuleTypes?: Type[];
enabled?: boolean; // undefined is to display all the rules
ruleExecutionStatus?: RuleExecutionStatus; // undefined means "all"
+ ruleSource?: RuleCustomizationEnum[]; // undefined is to display all the rules
}
export interface FetchRulesResponse {
@@ -202,3 +203,8 @@ export interface FindRulesReferencedByExceptionsProps {
lists: FindRulesReferencedByExceptionsListProp[];
signal?: AbortSignal;
}
+
+export enum RuleCustomizationEnum {
+ customized = 'CUSTOMIZED',
+ not_customized = 'NOT_CUSTOMIZED',
+}
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 aad1e053e15b6..6ec9ffdd02e67 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,8 +8,8 @@
import type { Dispatch, SetStateAction } from 'react';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { EuiButton, EuiToolTip } from '@elastic/eui';
+import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import type { RulesUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade';
-import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { RuleUpgradeConflictsResolverTab } from '../../../../rule_management/components/rule_details/three_way_diff/rule_upgrade_conflicts_resolver_tab';
import { PerFieldRuleDiffTab } from '../../../../rule_management/components/rule_details/per_field_rule_diff_tab';
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
@@ -111,13 +111,12 @@ interface UpgradePrebuiltRulesTableContextProviderProps {
export const UpgradePrebuiltRulesTableContextProvider = ({
children,
}: UpgradePrebuiltRulesTableContextProviderProps) => {
- const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled(
- 'prebuiltRulesCustomizationEnabled'
- );
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const [loadingRules, setLoadingRules] = useState([]);
const [filterOptions, setFilterOptions] = useState({
filter: '',
tags: [],
+ ruleSource: [],
});
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
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_filters.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx
index 215a810bf3aa2..900d81d0b0037 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx
@@ -9,10 +9,13 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isEqual } from 'lodash/fp';
import React, { useCallback } from 'react';
import styled from 'styled-components';
+import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
+import type { RuleCustomizationEnum } from '../../../../rule_management/logic';
import * as i18n from './translations';
import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover';
import { RuleSearchField } from '../rules_table_filters/rule_search_field';
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
+import { RuleCustomizationFilterPopover } from './upgrade_rule_customization_filter_popover';
const FilterWrapper = styled(EuiFlexGroup)`
margin-bottom: ${({ theme }) => theme.eui.euiSizeM};
@@ -28,7 +31,9 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => {
actions: { setFilterOptions },
} = useUpgradePrebuiltRulesTableContext();
- const { tags: selectedTags } = filterOptions;
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
+
+ const { tags: selectedTags, ruleSource: selectedRuleSource = [] } = filterOptions;
const handleOnSearch = useCallback(
(filterString: string) => {
@@ -52,22 +57,45 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => {
[selectedTags, setFilterOptions]
);
+ const handleSelectedRuleSource = useCallback(
+ (newRuleSource: RuleCustomizationEnum[]) => {
+ if (!isEqual(newRuleSource, selectedRuleSource)) {
+ setFilterOptions((filters) => ({
+ ...filters,
+ ruleSource: newRuleSource,
+ }));
+ }
+ },
+ [selectedRuleSource, setFilterOptions]
+ );
+
return (
-
+
-
-
-
+
+ {isPrebuiltRulesCustomizationEnabled && (
+
+
+
+ )}
+
+
+
+
);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx
new file mode 100644
index 0000000000000..234943e333272
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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, { useState, useMemo } from 'react';
+import type { EuiSelectableOption } from '@elastic/eui';
+import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui';
+import { RuleCustomizationEnum } from '../../../../rule_management/logic';
+import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations';
+import { toggleSelectedGroup } from '../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group';
+
+interface RuleCustomizationFilterPopoverProps {
+ selectedRuleSource: RuleCustomizationEnum[];
+ onSelectedRuleSourceChanged: (newRuleSource: RuleCustomizationEnum[]) => void;
+}
+
+const RULE_CUSTOMIZATION_POPOVER_WIDTH = 200;
+
+const RuleCustomizationFilterPopoverComponent = ({
+ selectedRuleSource,
+ onSelectedRuleSourceChanged,
+}: RuleCustomizationFilterPopoverProps) => {
+ const [isRuleCustomizationPopoverOpen, setIsRuleCustomizationPopoverOpen] = useState(false);
+
+ const selectableOptions: EuiSelectableOption[] = useMemo(
+ () => [
+ {
+ label: i18n.MODIFIED_LABEL,
+ key: RuleCustomizationEnum.customized,
+ checked: selectedRuleSource.includes(RuleCustomizationEnum.customized) ? 'on' : undefined,
+ },
+ {
+ label: i18n.UNMODIFIED_LABEL,
+ key: RuleCustomizationEnum.not_customized,
+ checked: selectedRuleSource.includes(RuleCustomizationEnum.not_customized)
+ ? 'on'
+ : undefined,
+ },
+ ],
+ [selectedRuleSource]
+ );
+
+ const handleSelectableOptionsChange = (
+ newOptions: EuiSelectableOption[],
+ _: unknown,
+ changedOption: EuiSelectableOption
+ ) => {
+ toggleSelectedGroup(
+ changedOption.key ?? '',
+ selectedRuleSource,
+ onSelectedRuleSourceChanged as (args: string[]) => void
+ );
+ };
+
+ const triggerButton = (
+ setIsRuleCustomizationPopoverOpen(!isRuleCustomizationPopoverOpen)}
+ numFilters={selectableOptions.length}
+ isSelected={isRuleCustomizationPopoverOpen}
+ hasActiveFilters={selectedRuleSource.length > 0}
+ numActiveFilters={selectedRuleSource.length}
+ data-test-subj="rule-customization-filter-popover-button"
+ >
+ {i18n.RULE_SOURCE}
+
+ );
+
+ return (
+ setIsRuleCustomizationPopoverOpen(!isRuleCustomizationPopoverOpen)}
+ panelPaddingSize="none"
+ repositionOnScroll
+ panelProps={{
+ 'data-test-subj': 'rule-customization-filter-popover',
+ }}
+ >
+
+ {(list) => {list}
}
+
+
+ );
+};
+
+export const RuleCustomizationFilterPopover = React.memo(RuleCustomizationFilterPopoverComponent);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts
index 342a1e6e8768e..b5a0e123d7510 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts
@@ -7,9 +7,12 @@
import { useMemo } from 'react';
import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules';
-import type { FilterOptions } from '../../../../rule_management/logic/types';
+import { RuleCustomizationEnum, type FilterOptions } from '../../../../rule_management/logic/types';
-export type UpgradePrebuiltRulesTableFilterOptions = Pick;
+export type UpgradePrebuiltRulesTableFilterOptions = Pick<
+ FilterOptions,
+ 'filter' | 'tags' | 'ruleSource'
+>;
export const useFilterPrebuiltRulesToUpgrade = ({
rules,
@@ -19,7 +22,7 @@ export const useFilterPrebuiltRulesToUpgrade = ({
filterOptions: UpgradePrebuiltRulesTableFilterOptions;
}) => {
const filteredRules = useMemo(() => {
- const { filter, tags } = filterOptions;
+ const { filter, tags, ruleSource } = filterOptions;
return rules.filter((ruleInfo) => {
if (filter && !ruleInfo.current_rule.name.toLowerCase().includes(filter.toLowerCase())) {
return false;
@@ -29,6 +32,25 @@ export const useFilterPrebuiltRulesToUpgrade = ({
return tags.every((tag) => ruleInfo.current_rule.tags.includes(tag));
}
+ if (ruleSource && ruleSource.length > 0) {
+ if (
+ ruleSource.includes(RuleCustomizationEnum.customized) &&
+ ruleSource.includes(RuleCustomizationEnum.not_customized)
+ ) {
+ return true;
+ } else if (
+ ruleSource.includes(RuleCustomizationEnum.customized) &&
+ ruleInfo.current_rule.rule_source.type === 'external'
+ ) {
+ return ruleInfo.current_rule.rule_source.is_customized;
+ } else if (
+ ruleSource.includes(RuleCustomizationEnum.not_customized) &&
+ ruleInfo.current_rule.rule_source.type === 'external'
+ ) {
+ return ruleInfo.current_rule.rule_source.is_customized === false;
+ }
+ }
+
return true;
});
}, [filterOptions, rules]);
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 29c5b2b201fe6..8c97a4ef52e2b 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,7 +6,7 @@
*/
import { useCallback, useMemo, useState } from 'react';
-import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
+import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import type {
RulesUpgradeState,
FieldsUpgradeState,
@@ -33,9 +33,7 @@ interface UseRulesUpgradeStateResult {
export function usePrebuiltRulesUpgradeState(
ruleUpgradeInfos: RuleUpgradeInfoForReview[]
): UseRulesUpgradeStateResult {
- const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled(
- 'prebuiltRulesCustomizationEnabled'
- );
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const [rulesResolvedConflicts, setRulesResolvedConflicts] = useState({});
const setRuleFieldResolvedValue = useCallback(
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 09009c98c2858..579f571f80e79 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
@@ -6,7 +6,14 @@
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
-import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui';
+import {
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiLink,
+ EuiLoadingSpinner,
+ EuiText,
+ EuiToolTip,
+} from '@elastic/eui';
import React, { useMemo } from 'react';
import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade/rule_upgrade_state';
import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name';
@@ -104,6 +111,35 @@ const INTEGRATIONS_COLUMN: TableColumn = {
truncateText: true,
};
+const MODIFIED_COLUMN: TableColumn = {
+ field: 'current_rule.rule_source',
+ name: ,
+ align: 'center',
+ render: (ruleSource: Rule['rule_source']) => {
+ if (
+ ruleSource == null ||
+ ruleSource.type === 'internal' ||
+ (ruleSource.type === 'external' && ruleSource.is_customized === false)
+ ) {
+ return null;
+ }
+
+ return (
+
+
+ {i18n.MODIFIED_LABEL}
+
+
+ );
+ },
+ width: '90px',
+ truncateText: true,
+};
+
const createUpgradeButtonColumn = (
upgradeRules: UpgradePrebuiltRulesTableActions['upgradeRules'],
loadingRules: RuleSignatureId[],
@@ -154,9 +190,15 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
} = useUpgradePrebuiltRulesTableContext();
const isDisabled = isRefetching || isUpgradingSecurityPackages;
+ // TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed
+ if (isPrebuiltRulesCustomizationEnabled) {
+ INTEGRATIONS_COLUMN.width = '70px';
+ }
+
return useMemo(
() => [
RULE_NAME_COLUMN,
+ ...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []),
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx
index c38fec638e478..ae24b2bb482d3 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx
@@ -46,6 +46,7 @@ import { useRulesTableActions } from './use_rules_table_actions';
import { MlRuleWarningPopover } from '../ml_rule_warning_popover/ml_rule_warning_popover';
import { getMachineLearningJobId } from '../../../../detections/pages/detection_engine/rules/helpers';
import type { TimeRange } from '../../../rule_gaps/types';
+import { useIsPrebuiltRulesCustomizationEnabled } from '../../../rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType;
@@ -233,6 +234,35 @@ const INTEGRATIONS_COLUMN: TableColumn = {
truncateText: true,
};
+const MODIFIED_COLUMN: TableColumn = {
+ field: 'rule_source',
+ name: ,
+ align: 'center',
+ render: (ruleSource: Rule['rule_source']) => {
+ if (
+ ruleSource == null ||
+ ruleSource.type === 'internal' ||
+ (ruleSource.type === 'external' && ruleSource.is_customized === false)
+ ) {
+ return null;
+ }
+
+ return (
+
+
+ {i18n.MODIFIED_LABEL}
+
+
+ );
+ },
+ width: '90px',
+ truncateText: true,
+};
+
const useActionsColumn = ({
showExceptionsDuplicateConfirmation,
showManualRuleRunConfirmation,
@@ -265,6 +295,7 @@ export const useRulesColumns = ({
});
const ruleNameColumn = useRuleNameColumn();
const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING);
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const enabledColumn = useEnabledColumn({
hasCRUDPermissions,
isLoadingJobs,
@@ -279,9 +310,15 @@ export const useRulesColumns = ({
});
const snoozeColumn = useRuleSnoozeColumn();
+ // TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed
+ if (isPrebuiltRulesCustomizationEnabled) {
+ INTEGRATIONS_COLUMN.width = '70px';
+ }
+
return useMemo(
() => [
ruleNameColumn,
+ ...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []),
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
@@ -352,13 +389,14 @@ export const useRulesColumns = ({
...(hasCRUDPermissions ? [actionsColumn] : []),
],
[
- actionsColumn,
- enabledColumn,
+ ruleNameColumn,
+ isPrebuiltRulesCustomizationEnabled,
+ showRelatedIntegrations,
executionStatusColumn,
snoozeColumn,
+ enabledColumn,
hasCRUDPermissions,
- ruleNameColumn,
- showRelatedIntegrations,
+ actionsColumn,
]
);
};
@@ -380,6 +418,7 @@ export const useMonitoringColumns = ({
});
const ruleNameColumn = useRuleNameColumn();
const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING);
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const enabledColumn = useEnabledColumn({
hasCRUDPermissions,
isLoadingJobs,
@@ -393,12 +432,18 @@ export const useMonitoringColumns = ({
mlJobs,
});
+ // TODO: move this change to the `INTEGRATIONS_COLUMN` when `prebuiltRulesCustomizationEnabled` feature flag is removed
+ if (isPrebuiltRulesCustomizationEnabled) {
+ INTEGRATIONS_COLUMN.width = '70px';
+ }
+
return useMemo(
() => [
{
...ruleNameColumn,
width: '28%',
},
+ ...(isPrebuiltRulesCustomizationEnabled ? [MODIFIED_COLUMN] : []),
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
@@ -503,6 +548,7 @@ export const useMonitoringColumns = ({
enabledColumn,
executionStatusColumn,
hasCRUDPermissions,
+ isPrebuiltRulesCustomizationEnabled,
ruleNameColumn,
showRelatedIntegrations,
]
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx
index 2d2875d5a8734..49b6c1d1e4e99 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/related_integrations/integrations_popover/index.tsx
@@ -16,6 +16,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
+import { useIsPrebuiltRulesCustomizationEnabled } from '../../../../../detection_engine/rule_management/hooks/use_is_prebuilt_rules_customization_enabled';
import type { RelatedIntegrationArray } from '../../../../../../common/api/detection_engine/model/rule_schema';
import { IntegrationDescription } from '../integrations_description';
import { useRelatedIntegrations } from '../use_related_integrations';
@@ -54,6 +55,7 @@ const IntegrationListItem = styled('li')`
const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopoverProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const { integrations, isLoaded } = useRelatedIntegrations(relatedIntegrations);
+ const isPrebuiltRulesCustomizationEnabled = useIsPrebuiltRulesCustomizationEnabled();
const enabledIntegrations = useMemo(() => {
return integrations.filter(
@@ -65,10 +67,13 @@ const IntegrationsPopoverComponent = ({ relatedIntegrations }: IntegrationsPopov
const numIntegrationsEnabled = enabledIntegrations.length;
const badgeTitle = useMemo(() => {
+ if (isPrebuiltRulesCustomizationEnabled) {
+ return isLoaded ? `${numIntegrationsEnabled}/${numIntegrations}` : `${numIntegrations}`;
+ }
return isLoaded
? `${numIntegrationsEnabled}/${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`
: `${numIntegrations} ${i18n.INTEGRATIONS_BADGE}`;
- }, [isLoaded, numIntegrations, numIntegrationsEnabled]);
+ }, [isLoaded, isPrebuiltRulesCustomizationEnabled, numIntegrations, numIntegrationsEnabled]);
return (
{
+ describe('Upgrade of prebuilt rules with conflicts', () => {
+ const RULE_1_ID = 'rule_1';
+ const RULE_2_ID = 'rule_2';
+ const OUTDATED_RULE_1 = createRuleAssetSavedObject({
+ name: 'Outdated rule 1',
+ rule_id: RULE_1_ID,
+ version: 1,
+ });
+ const UPDATED_RULE_1 = createRuleAssetSavedObject({
+ name: 'Updated rule 1',
+ rule_id: RULE_1_ID,
+ version: 2,
+ });
+ const OUTDATED_RULE_2 = createRuleAssetSavedObject({
+ name: 'Outdated rule 2',
+ rule_id: RULE_2_ID,
+ version: 1,
+ });
+ const UPDATED_RULE_2 = createRuleAssetSavedObject({
+ name: 'Updated rule 2',
+ rule_id: RULE_2_ID,
+ version: 2,
+ });
+ const patchedName = 'A new name that creates a conflict';
+ beforeEach(() => {
+ login();
+ resetRulesTableState();
+ deleteAlertsAndRules();
+ cy.intercept('POST', '/internal/detection_engine/prebuilt_rules/upgrade/_perform').as(
+ 'updatePrebuiltRules'
+ );
+ /* Create a new rule and install it */
+ createAndInstallMockedPrebuiltRules([OUTDATED_RULE_1, OUTDATED_RULE_2]);
+ /* Modify one of the rule's name to cause a conflict */
+ patchRule(OUTDATED_RULE_1['security-rule'].rule_id, {
+ name: patchedName,
+ });
+ /* Create a second version of the rule, making it available for update */
+ installPrebuiltRuleAssets([UPDATED_RULE_1, UPDATED_RULE_2]);
+
+ visitRulesManagementTable();
+ clickRuleUpdatesTab();
+ });
+
+ it('should filter by customized prebuilt rules', () => {
+ // Filter table to show modified rules only
+ filterPrebuiltRulesUpdateTableByRuleCustomization('Modified');
+ cy.get(MODIFIED_RULE_BADGE).should('exist');
+
+ // Verify only rules with customized rule sources are displayed
+ cy.get(RULES_UPDATES_TABLE).contains(patchedName);
+ assertRulesNotPresentInRuleUpdatesTable([OUTDATED_RULE_2]);
+ });
+
+ it('should filter by customized prebuilt rules', () => {
+ // Filter table to show unmodified rules only
+ filterPrebuiltRulesUpdateTableByRuleCustomization('Unmodified');
+ cy.get(MODIFIED_RULE_BADGE).should('not.exist');
+
+ // Verify only rules with non-customized rule sources are displayed
+ assertRulesPresentInRuleUpdatesTable([OUTDATED_RULE_2]);
+ cy.get(patchedName).should('not.exist');
+ });
+ });
+ }
+);
diff --git a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts
index bbc7a346b252f..e2ce41dee2847 100644
--- a/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts
+++ b/x-pack/test/security_solution_cypress/cypress/screens/alerts_detection_rules.ts
@@ -349,3 +349,5 @@ export const ESQL_QUERY_VALUE = '[data-test-subj="esqlQueryPropertyValue"]';
export const PER_FIELD_DIFF_WRAPPER = '[data-test-subj="ruleUpgradePerFieldDiffWrapper"]';
export const PER_FIELD_DIFF_DEFINITION_SECTION = '[data-test-subj="perFieldDiffDefinitionSection"]';
+
+export const MODIFIED_RULE_BADGE = '[data-test-subj="upgradeRulesTableModifiedColumnBadge"]';
diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts
new file mode 100644
index 0000000000000..4b11a4624c3e2
--- /dev/null
+++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_updates.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+export const RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON =
+ '[data-test-subj="rule-customization-filter-popover-button"]';
+
+export const RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL =
+ '[data-test-subj="rule-customization-filter-popover"]';
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts
index 008fc92bd0224..c0ef625f52e35 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/rules.ts
@@ -39,6 +39,23 @@ export const createRule = (
);
};
+export const patchRule = (
+ ruleId: string,
+ updateData: Partial
+): Cypress.Chainable> => {
+ return cy.currentSpace().then((spaceId) =>
+ rootRequest({
+ method: 'PATCH',
+ url: spaceId ? getSpaceUrl(spaceId, DETECTION_ENGINE_RULES_URL) : DETECTION_ENGINE_RULES_URL,
+ body: {
+ rule_id: ruleId,
+ ...updateData,
+ },
+ failOnStatusCode: false,
+ })
+ );
+};
+
/**
* Snoozes a rule via API
*
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts
index b4f68dba976c3..d4148d5e632a1 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts
@@ -17,6 +17,10 @@ import {
TOASTER,
} from '../screens/alerts_detection_rules';
import type { SAMPLE_PREBUILT_RULE } from './api_calls/prebuilt_rules';
+import {
+ RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON,
+ RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL,
+} from '../screens/rule_updates';
export const clickAddElasticRulesButton = () => {
cy.get(ADD_ELASTIC_RULES_BTN).click();
@@ -150,3 +154,9 @@ export const assertRulesNotPresentInRuleUpdatesTable = (
cy.get(rule['security-rule'].name).should('not.exist');
}
};
+
+export const filterPrebuiltRulesUpdateTableByRuleCustomization = (text: string) => {
+ cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON).click();
+ cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_PANEL).contains(text).click();
+ cy.get(RULE_UPGRADE_TABLE_MODIFICATION_FILTER_BUTTON).click();
+};