diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx index 38b44047b955e..ddcbab8dbb61c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.test.tsx @@ -19,14 +19,14 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { StepAboutRule } from '../../../rule_creation_ui/components/step_about_rule'; import type { AboutStepRule } from '../../../common/types'; import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; -import { usePrebuiltRuleBaseVersionContext } from '../../../rule_management/components/rule_details/base_version_diff/base_version_context'; +import { useRuleCustomizationsContext } from '../../../rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context'; jest.mock('../../../../common/lib/kibana'); jest.mock( - '../../../rule_management/components/rule_details/base_version_diff/base_version_context' + '../../../rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context' ); -const usePrebuiltRuleBaseVersionContextMock = usePrebuiltRuleBaseVersionContext as jest.Mock; +const useRuleCustomizationsContextMock = useRuleCustomizationsContext as jest.Mock; const mockTheme = getMockTheme({ eui: { euiSizeL: '10px', euiBreakpoints: { s: '450px' }, euiSizeM: '10px' }, @@ -37,7 +37,7 @@ describe('StepAboutRuleToggleDetails', () => { beforeEach(() => { stepDataMock = mockAboutStepRule(); - usePrebuiltRuleBaseVersionContextMock.mockReturnValue({ + useRuleCustomizationsContextMock.mockReturnValue({ actions: { openCustomizationsPreviewFlyout: jest.fn() }, state: { doesBaseVersionExist: true, modifiedFields: new Set() }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx index 9b86181c3786f..30735acb67d81 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_about_rule_details/index.tsx @@ -29,7 +29,6 @@ import type { AboutStepRule, AboutStepRuleDetails } from '../../../common/types' import * as i18n from './translations'; import { fullHeight } from './styles'; import type { RuleResponse } from '../../../../../common/api/detection_engine'; -import { ModifiedFieldBadge } from '../../../rule_management/components/rule_details/modified_field_badge'; import { RuleFieldName } from '../../../rule_management/components/rule_details/rule_field_name'; const detailsOption: EuiButtonGroupOptionProps = { @@ -47,7 +46,6 @@ const setupOption: EuiButtonGroupOptionProps = { label: i18n.ABOUT_PANEL_SETUP_TAB, 'data-test-subj': 'stepAboutDetailsToggle-setup', }; - interface StepPanelProps { stepData: AboutStepRule | null; stepDataDetails: AboutStepRuleDetails | null; @@ -79,7 +77,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ ...(notesExist ? [notesOption] : []), ...(setupExists ? [setupOption] : []), ]; - }, [stepDataDetails]); + }, [stepDataDetails?.note, stepDataDetails?.setup]); return ( = ({ - + )} )} {selectedToggleOption === 'notes' && ( - + - - - {stepDataDetails.note} - + )} {selectedToggleOption === 'setup' && ( - + - - - {stepDataDetails.setup} - + )} @@ -164,12 +150,10 @@ export const StepAboutRuleToggleDetails = memo(StepAboutRuleToggleDetailsCompone interface VerticalOverflowContainerProps { maxHeight: number; - 'data-test-subj'?: string; } function VerticalOverflowContainer({ maxHeight, - 'data-test-subj': dataTestSubject, children, }: PropsWithChildren): JSX.Element { return ( @@ -179,7 +163,6 @@ function VerticalOverflowContainer({ overflow-y: hidden; word-break: break-word; `} - data-test-subj={dataTestSubject} > {children} @@ -209,13 +192,7 @@ const RuleDescription = ({ description }: { description: string }) => ( - ), + title: , description: ( {description} @@ -225,3 +202,27 @@ const RuleDescription = ({ description }: { description: string }) => ( ]} /> ); + +const RuleInvestigationGuide = ({ note }: { note: string }) => ( + , + description: {note}, + }, + ]} + descriptionProps={{ 'data-test-subj': 'stepAboutDetailsNoteContent' }} + /> +); + +const RuleSetupGuide = ({ setup }: { setup: string }) => ( + , + description: {setup}, + }, + ]} + descriptionProps={{ 'data-test-subj': 'stepAboutDetailsSetupContent' }} + /> +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 0311f74da1399..7045aa22e7619 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -22,7 +22,7 @@ import type { Filter } from '@kbn/es-query'; import { Route, Routes } from '@kbn/shared-ux-router'; import { noop } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react'; import { useParams } from 'react-router-dom'; import type { ConnectedProps } from 'react-redux'; import { connect, useDispatch } from 'react-redux'; @@ -38,10 +38,7 @@ import { tableDefaults, TableId, } from '@kbn/securitysolution-data-table'; -import { - PrebuiltRuleBaseVersionFlyoutContextProvider, - usePrebuiltRuleBaseVersionContext, -} from '../../../rule_management/components/rule_details/base_version_diff/base_version_context'; +import { RuleCustomizationsContextProvider } from '../../../rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context'; import { useGroupTakeActionsItems } from '../../../../detections/hooks/alerts_table/use_group_take_action_items'; import { useDataView } from '../../../../data_view_manager/hooks/use_data_view'; import { @@ -207,701 +204,694 @@ const defaultGroupingOptions = [ }, ]; -type DetectionEngineComponentProps = PropsFromRedux; - -const RuleDetailsPageComponent: React.FC = ({ - clearEventsDeleted, - clearEventsLoading, - clearSelected, -}) => { - const { - analytics, - i18n: i18nStart, - theme, - application: { - navigateToApp, - capabilities: { actions }, - }, - timelines: timelinesUi, - spaces: spacesApi, - } = useKibana().services; - - const dispatch = useDispatch(); - const containerElement = useRef(null); - const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); - - const updatedAt = useShallowEqualSelector( - (state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).updated - ); - const isAlertsLoading = useShallowEqualSelector( - (state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).isLoading - ); - const getGlobalFiltersQuerySelector = useMemo( - () => inputsSelectors.globalFiltersQuerySelector(), - [] - ); - const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); - const query = useDeepEqualSelector(getGlobalQuerySelector); - const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); - - const { to, from } = useGlobalTime(); - const [ - { - loading: userInfoLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexRead, - signalIndexName, - hasIndexWrite, - hasIndexMaintenance, - }, - ] = useUserData(); - const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = - useListsConfig(); - - const { sourcererDataView: oldSourcererDataViewSpec, loading: oldIsLoadingIndexPattern } = - useSourcererDataView(SourcererScopeName.detections); - const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); - const { dataView: experimentalDataView, status } = useDataView(SourcererScopeName.detections); - const isLoadingIndexPattern = newDataViewPickerEnabled - ? status !== 'ready' - : oldIsLoadingIndexPattern; - - const loading = userInfoLoading || listsConfigLoading; - const { detailName: ruleId } = useParams<{ - detailName: string; - tabName: string; - }>(); - - const { - rule: maybeRule, - refresh: refreshRule, - loading: ruleLoading, - isExistingRule, - } = useRuleWithFallback(ruleId); - - const { pollForSignalIndex } = useSignalHelpers(); - const [rule, setRule] = useState(null); - const isLoading = useMemo(() => ruleLoading && rule == null, [rule, ruleLoading]); - - const { starting: isStartingJobs, startMlJobs } = useStartMlJobs(); - const startMlJobsIfNeeded = useCallback(async () => { - if (rule) { - await startMlJobs(getMachineLearningJobId(rule)); - } - }, [rule, startMlJobs]); - - const pageTabs = useRuleDetailsTabs({ rule, ruleId, isExistingRule, hasIndexRead }); - - const [isDeleteConfirmationVisible, showDeleteConfirmation, hideDeleteConfirmation] = - useBoolState(); - - const [confirmDeletion, handleDeletionConfirm, handleDeletionCancel] = useAsyncConfirmation({ - onInit: showDeleteConfirmation, - onFinish: hideDeleteConfirmation, - }); - - const { aboutRuleData, modifiedAboutRuleDetailsData, ruleActionsData } = - rule != null - ? getStepsData({ rule, detailsView: true }) - : { - aboutRuleData: null, - modifiedAboutRuleDetailsData: null, - ruleActionsData: null, - }; - - const { showBuildingBlockAlerts, setShowBuildingBlockAlerts, showOnlyThreatIndicatorAlerts } = - useDataTableFilters(TableId.alertsOnRuleDetailsPage); - - const mlCapabilities = useMlCapabilities(); - const { globalFullScreen } = useGlobalFullScreen(); - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const storeGapsInEventLogEnabled = useIsExperimentalFeatureEnabled('storeGapsInEventLogEnabled'); - // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); - - const hasActionsPrivileges = useMemo(() => { - if (rule?.actions != null && rule?.actions.length > 0 && isBoolean(actions.show)) { - return actions.show; - } - return true; - }, [actions, rule?.actions]); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(dataTableActions.clearSelected({ id })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(dataTableActions.clearEventsLoading({ id })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(dataTableActions.clearEventsDeleted({ id })), +}); +const connector = connect(null, mapDispatchToProps); + +type DetectionEngineComponentProps = ConnectedProps; + +export const RuleDetailsPage = connector( + memo(function RuleDetailsPage({ + clearEventsDeleted, + clearEventsLoading, + clearSelected, + }: DetectionEngineComponentProps) { + const { + analytics, + i18n: i18nStart, + theme, + application: { + navigateToApp, + capabilities: { actions }, + }, + timelines: timelinesUi, + spaces: spacesApi, + } = useKibana().services; + + const dispatch = useDispatch(); + const containerElement = useRef(null); + const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); + + const updatedAt = useShallowEqualSelector( + (state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).updated + ); + const isAlertsLoading = useShallowEqualSelector( + (state) => (getTable(state, TableId.alertsOnRuleDetailsPage) ?? tableDefaults).isLoading + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from } = useGlobalTime(); + const [ + { + loading: userInfoLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexRead, + signalIndexName, + hasIndexWrite, + hasIndexMaintenance, + }, + ] = useUserData(); + const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = + useListsConfig(); + + const { sourcererDataView: oldSourcererDataViewSpec, loading: oldIsLoadingIndexPattern } = + useSourcererDataView(SourcererScopeName.detections); + const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled'); + const { dataView: experimentalDataView, status } = useDataView(SourcererScopeName.detections); + const isLoadingIndexPattern = newDataViewPickerEnabled + ? status !== 'ready' + : oldIsLoadingIndexPattern; + + const loading = userInfoLoading || listsConfigLoading; + const { detailName: ruleId } = useParams<{ + detailName: string; + tabName: string; + }>(); + + const { + rule: maybeRule, + refresh: refreshRule, + loading: ruleLoading, + isExistingRule, + } = useRuleWithFallback(ruleId); + + const { pollForSignalIndex } = useSignalHelpers(); + const [rule, setRule] = useState(null); + const isLoading = useMemo(() => ruleLoading && rule == null, [rule, ruleLoading]); + + const { starting: isStartingJobs, startMlJobs } = useStartMlJobs(); + const startMlJobsIfNeeded = useCallback(async () => { + if (rule) { + await startMlJobs(getMachineLearningJobId(rule)); + } + }, [rule, startMlJobs]); - const navigateToAlertsTab = useCallback(() => { - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsTabUrl(ruleId ?? '', 'alerts', ''), + const pageTabs = useRuleDetailsTabs({ rule, ruleId, isExistingRule, hasIndexRead }); + + const [isDeleteConfirmationVisible, showDeleteConfirmation, hideDeleteConfirmation] = + useBoolState(); + + const [confirmDeletion, handleDeletionConfirm, handleDeletionCancel] = useAsyncConfirmation({ + onInit: showDeleteConfirmation, + onFinish: hideDeleteConfirmation, }); - }, [navigateToApp, ruleId]); - const { - actions: { setBaseVersionRule }, - } = usePrebuiltRuleBaseVersionContext(); + const { aboutRuleData, modifiedAboutRuleDetailsData, ruleActionsData } = + rule != null + ? getStepsData({ rule, detailsView: true }) + : { + aboutRuleData: null, + modifiedAboutRuleDetailsData: null, + ruleActionsData: null, + }; + + const { showBuildingBlockAlerts, setShowBuildingBlockAlerts, showOnlyThreatIndicatorAlerts } = + useDataTableFilters(TableId.alertsOnRuleDetailsPage); + + const mlCapabilities = useMlCapabilities(); + const { globalFullScreen } = useGlobalFullScreen(); + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const storeGapsInEventLogEnabled = useIsExperimentalFeatureEnabled( + 'storeGapsInEventLogEnabled' + ); + // TODO: Refactor license check + hasMlAdminPermissions to common check + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); - // persist rule until refresh is complete - useEffect(() => { - if (maybeRule != null) { - setRule(maybeRule); - setBaseVersionRule(maybeRule); - } - }, [maybeRule, setBaseVersionRule]); + const hasActionsPrivileges = useMemo(() => { + if (rule?.actions != null && rule?.actions.length > 0 && isBoolean(actions.show)) { + return actions.show; + } + return true; + }, [actions, rule?.actions]); + + const navigateToAlertsTab = useCallback(() => { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsTabUrl(ruleId ?? '', 'alerts', ''), + }); + }, [navigateToApp, ruleId]); + + // persist rule until refresh is complete + useEffect(() => { + if (maybeRule != null) { + setRule(maybeRule); + } + }, [maybeRule]); - useLegacyUrlRedirect({ rule, spacesApi }); + useLegacyUrlRedirect({ rule, spacesApi }); - const showUpdating = useMemo( - () => isLoadingIndexPattern || isAlertsLoading || loading, - [isLoadingIndexPattern, isAlertsLoading, loading] - ); + const showUpdating = useMemo( + () => isLoadingIndexPattern || isAlertsLoading || loading, + [isLoadingIndexPattern, isAlertsLoading, loading] + ); - const title = useMemo( - () => ( - <> - {rule?.name} {ruleLoading && } - - ), - [rule, ruleLoading] - ); - const badgeOptions = useMemo( - () => - !ruleLoading && !isExistingRule - ? { - text: i18n.DELETED_RULE, - color: 'default', - } - : undefined, - [isExistingRule, ruleLoading] - ); - const subTitle = useMemo( - () => - rule ? ( - [ - , - rule?.updated_by != null ? ( - - ) : ( - '' - ), - ] - ) : ruleLoading ? ( - - ) : null, - [rule, ruleLoading] - ); - - // Callback for when open/closed filter changes - const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: Status) => { - const tableId = TableId.alertsOnRuleDetailsPage; - clearEventsLoading({ id: tableId }); - clearEventsDeleted({ id: tableId }); - clearSelected({ id: tableId }); - setFilterGroup(newFilterGroup); - }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] - ); - - const isBuildingBlockTypeNotNull = rule?.building_block_type != null; - // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts - useEffect(() => { - setShowBuildingBlockAlerts(isBuildingBlockTypeNotNull); - }, [isBuildingBlockTypeNotNull, setShowBuildingBlockAlerts]); - - const ruleRuleId = rule?.rule_id ?? ''; - const alertDefaultFilters = useMemo( - () => [ - ...buildAlertsFilter(ruleRuleId ?? ''), - ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), - ...buildAlertStatusFilter(filterGroup), - ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), - ], - [ruleRuleId, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts, filterGroup] - ); - - const alertMergedFilters = useMemo( - () => [...alertDefaultFilters, ...filters], - [alertDefaultFilters, filters] - ); - - const lastExecution = rule?.execution_summary?.last_execution; - const lastExecutionStatus = lastExecution?.status; - const lastExecutionDate = lastExecution?.date ?? ''; - const lastExecutionMessage = lastExecution?.message ?? ''; - - const upgradeCallout = useRuleUpdateCallout({ - rule, - message: ruleI18n.HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE, - onUpgrade: refreshRule, - }); - - const ruleStatusInfo = useMemo(() => { - return ( - <> - {ruleLoading ? ( - - - - ) : ( - - - - )} - - - - + const title = useMemo( + () => ( + <> + {rule?.name} {ruleLoading && } + + ), + [rule, ruleLoading] ); - }, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); - - // Extract rule index if available on rule type - let ruleIndex: string[] | undefined; - if (rule != null && 'index' in rule && Array.isArray(rule.index)) { - ruleIndex = rule.index; - } - - const ruleError = useMemo(() => { - return ruleLoading ? ( - - - - ) : ( - + const badgeOptions = useMemo( + () => + !ruleLoading && !isExistingRule + ? { + text: i18n.DELETED_RULE, + color: 'default', + } + : undefined, + [isExistingRule, ruleLoading] ); - }, [ - lastExecutionStatus, - lastExecutionDate, - lastExecutionMessage, - ruleLoading, - rule?.immutable, - rule?.name, - ruleIndex, - ]); - - const updateDateRangeCallback = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - dispatch( - setAbsoluteRangeDatePicker({ - id: InputsModelId.global, - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }) - ); - }, - [dispatch] - ); - - const handleOnChangeEnabledRule = useCallback((enabled: boolean) => { - setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule)); - }, []); - - const onSkipFocusBeforeEventsTable = useCallback(() => { - focusUtilityBarAction(containerElement.current); - }, [containerElement]); - - const onSkipFocusAfterEventsTable = useCallback(() => { - resetKeyboardFocus(); - }, []); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (isTab(keyboardEvent)) { - onTimelineTabKeyPressed({ - containerElement: containerElement.current, - keyboardEvent, - onSkipFocusBeforeEventsTable, - onSkipFocusAfterEventsTable, - }); - } - }, - [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] - ); - const currentAlertStatusFilterValue = useMemo(() => [filterGroup], [filterGroup]); - const updatedAtValue = useMemo(() => { - return timelinesUi.getLastUpdated({ - updatedAt: updatedAt || Date.now(), - showUpdating, + const subTitle = useMemo( + () => + rule ? ( + [ + , + rule?.updated_by != null ? ( + + ) : ( + '' + ), + ] + ) : ruleLoading ? ( + + ) : null, + [rule, ruleLoading] + ); + + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: Status) => { + const tableId = TableId.alertsOnRuleDetailsPage; + clearEventsLoading({ id: tableId }); + clearEventsDeleted({ id: tableId }); + clearSelected({ id: tableId }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + + const isBuildingBlockTypeNotNull = rule?.building_block_type != null; + // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts + useEffect(() => { + setShowBuildingBlockAlerts(isBuildingBlockTypeNotNull); + }, [isBuildingBlockTypeNotNull, setShowBuildingBlockAlerts]); + + const ruleRuleId = rule?.rule_id ?? ''; + const alertDefaultFilters = useMemo( + () => [ + ...buildAlertsFilter(ruleRuleId ?? ''), + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildAlertStatusFilter(filterGroup), + ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), + ], + [ruleRuleId, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts, filterGroup] + ); + + const alertMergedFilters = useMemo( + () => [...alertDefaultFilters, ...filters], + [alertDefaultFilters, filters] + ); + + const lastExecution = rule?.execution_summary?.last_execution; + const lastExecutionStatus = lastExecution?.status; + const lastExecutionDate = lastExecution?.date ?? ''; + const lastExecutionMessage = lastExecution?.message ?? ''; + + const upgradeCallout = useRuleUpdateCallout({ + rule, + message: ruleI18n.HAS_RULE_UPDATE_DETAILS_CALLOUT_MESSAGE, + onUpgrade: refreshRule, }); - }, [updatedAt, showUpdating, timelinesUi]); - const renderGroupedAlertTable = useCallback( - (groupingFilters: Filter[]) => { + const ruleStatusInfo = useMemo(() => { return ( - + {ruleLoading ? ( + + + + ) : ( + + + + )} + + + + + ); + }, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); + + // Extract rule index if available on rule type + let ruleIndex: string[] | undefined; + if (rule != null && 'index' in rule && Array.isArray(rule.index)) { + ruleIndex = rule.index; + } + + const ruleError = useMemo(() => { + return ruleLoading ? ( + + + + ) : ( + ); - }, - [alertMergedFilters, refreshRule] - ); - - const { - isBulkDuplicateConfirmationVisible, - showBulkDuplicateConfirmation, - cancelRuleDuplication, - confirmRuleDuplication, - } = useBulkDuplicateExceptionsConfirmation(); - - const { - isManualRuleRunConfirmationVisible, - showManualRuleRunConfirmation, - cancelManualRuleRun, - confirmManualRuleRun, - } = useManualRuleRunConfirmation(); - - const groupTakeActionItems = useGroupTakeActionsItems({ - currentStatus: currentAlertStatusFilterValue, - showAlertStatusActions: Boolean(hasIndexWrite) && Boolean(hasIndexMaintenance), - }); - - const accordionExtraActionGroupStats = useMemo( - () => ({ - aggregations: defaultGroupStatsAggregations, - renderer: defaultGroupStatsRenderer, - }), - [] - ); - - if ( - redirectToDetections( - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - needsListsConfiguration - ) - ) { - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.alerts, - path: getDetectionEngineUrl(), + }, [ + lastExecutionStatus, + lastExecutionDate, + lastExecutionMessage, + ruleLoading, + rule?.immutable, + rule?.name, + ruleIndex, + ]); + + const updateDateRangeCallback = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + dispatch( + setAbsoluteRangeDatePicker({ + id: InputsModelId.global, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); + }, + [dispatch] + ); + + const handleOnChangeEnabledRule = useCallback((enabled: boolean) => { + setRule((currentRule) => (currentRule ? { ...currentRule, enabled } : currentRule)); + }, []); + + const onSkipFocusBeforeEventsTable = useCallback(() => { + focusUtilityBarAction(containerElement.current); + }, [containerElement]); + + const onSkipFocusAfterEventsTable = useCallback(() => { + resetKeyboardFocus(); + }, []); + + const onKeyDown = useCallback( + (keyboardEvent: React.KeyboardEvent) => { + if (isTab(keyboardEvent)) { + onTimelineTabKeyPressed({ + containerElement: containerElement.current, + keyboardEvent, + onSkipFocusBeforeEventsTable, + onSkipFocusAfterEventsTable, + }); + } + }, + [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] + ); + const currentAlertStatusFilterValue = useMemo(() => [filterGroup], [filterGroup]); + const updatedAtValue = useMemo(() => { + return timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + showUpdating, + }); + }, [updatedAt, showUpdating, timelinesUi]); + + const renderGroupedAlertTable = useCallback( + (groupingFilters: Filter[]) => { + return ( + + ); + }, + [alertMergedFilters, refreshRule] + ); + + const { + isBulkDuplicateConfirmationVisible, + showBulkDuplicateConfirmation, + cancelRuleDuplication, + confirmRuleDuplication, + } = useBulkDuplicateExceptionsConfirmation(); + + const { + isManualRuleRunConfirmationVisible, + showManualRuleRunConfirmation, + cancelManualRuleRun, + confirmManualRuleRun, + } = useManualRuleRunConfirmation(); + + const groupTakeActionItems = useGroupTakeActionsItems({ + currentStatus: currentAlertStatusFilterValue, + showAlertStatusActions: Boolean(hasIndexWrite) && Boolean(hasIndexMaintenance), }); - return null; - } - - const defaultRuleStackByOption: AlertsStackByField = 'event.category'; - - const hasNotificationActions = ruleActionsData != null && ruleActionsData.actions.length > 0; - const hasResponseActions = - ruleActionsData != null && (ruleActionsData.responseActions || []).length > 0; - const hasActions = hasNotificationActions || hasResponseActions; - - const isRuleEnabled = isExistingRule && (rule?.enabled ?? false); - - return ( - <> - - - {upgradeCallout} - {isBulkDuplicateConfirmationVisible && ( - - )} - {isDeleteConfirmationVisible && ( - handleDeletionConfirm()} - confirmButtonText={ruleI18n.DELETE_CONFIRMATION_CONFIRM} - cancelButtonText={ruleI18n.DELETE_CONFIRMATION_CANCEL} - buttonColor="danger" - defaultFocusedButton="confirm" - data-test-subj="deleteRulesConfirmationModal" - > - {i18n.DELETE_CONFIRMATION_BODY} - - )} - {isManualRuleRunConfirmationVisible && ( - - )} - - - - ({ + aggregations: defaultGroupStatsAggregations, + renderer: defaultGroupStatsRenderer, + }), + [] + ); + + if ( + redirectToDetections( + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + needsListsConfiguration + ) + ) { + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); + return null; + } + + const defaultRuleStackByOption: AlertsStackByField = 'event.category'; + + const hasNotificationActions = ruleActionsData != null && ruleActionsData.actions.length > 0; + const hasResponseActions = + ruleActionsData != null && (ruleActionsData.responseActions || []).length > 0; + const hasActions = hasNotificationActions || hasResponseActions; + + const isRuleEnabled = isExistingRule && (rule?.enabled ?? false); + + return ( + <> + + + {upgradeCallout} + {isBulkDuplicateConfirmationVisible && ( + - - - - - - - - - {ruleStatusI18n.STATUS} - {':'} - - {ruleStatusInfo} - - - } - title={title} - badgeOptions={badgeOptions} - > - - - - - - {i18n.ENABLE_RULE} + )} + {isDeleteConfirmationVisible && ( + handleDeletionConfirm()} + confirmButtonText={ruleI18n.DELETE_CONFIRMATION_CONFIRM} + cancelButtonText={ruleI18n.DELETE_CONFIRMATION_CANCEL} + buttonColor="danger" + defaultFocusedButton="confirm" + data-test-subj="deleteRulesConfirmationModal" + > + {i18n.DELETE_CONFIRMATION_BODY} + + )} + {isManualRuleRunConfirmationVisible && ( + + )} + + + + + + + + + + + + + + {ruleStatusI18n.STATUS} + {':'} + + {ruleStatusInfo} + - - - - - + } + title={title} + badgeOptions={badgeOptions} + > + - + > + + + {i18n.ENABLE_RULE} + + + - - - - - - - {ruleError} - - - - - - {rule !== null && ( - - )} - - - - - - - {rule !== null && !isStartingJobs && ( - + + + + + - )} - + + - - - - {rule != null && } - - - {hasActions && ( - - - - - - )} - - - - - - - - - - - <> - - - + + {ruleError} + + + + + + {rule !== null && ( + + )} + + + + + + + {rule !== null && !isStartingJobs && ( + + )} + + + + + + {rule != null && } + + + {hasActions && ( + + + + + + )} + - {updatedAtValue} - - - + + + + + + + + <> + + + + + {updatedAtValue} + + + + + + + {ruleId != null && ( + + )} + + + + - - - {ruleId != null && ( - + + - )} - - - - - - - - - - <> - - - {storeGapsInEventLogEnabled && ( + + <> - + + {storeGapsInEventLogEnabled && ( + <> + + + + )} + - )} - - - - - - - - - - - - - - ); -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSelected: ({ id }: { id: string }) => dispatch(dataTableActions.clearSelected({ id })), - clearEventsLoading: ({ id }: { id: string }) => - dispatch(dataTableActions.clearEventsLoading({ id })), - clearEventsDeleted: ({ id }: { id: string }) => - dispatch(dataTableActions.clearEventsDeleted({ id })), -}); - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; - -const ConnectedRuleDetailsPage = connector(React.memo(RuleDetailsPageComponent)); - -export const RuleDetailsPage = () => ( - - - + + + + + + + + + + + + + ); + }) ); - -RuleDetailsPage.displayName = 'RuleDetailsPage'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx index f9b0d04b36a6f..4df13ee49bb83 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.test.tsx @@ -14,7 +14,7 @@ import { useBulkExport } from '../../../../rule_management/logic/bulk_actions/us import { useExecuteBulkAction } from '../../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; import { mockRule } from '../../../../rule_management_ui/components/rules_table/__mocks__/mock'; import type { ExternalRuleSource } from '../../../../../../common/api/detection_engine'; -import { usePrebuiltRuleBaseVersionContext } from '../../../../rule_management/components/rule_details/base_version_diff/base_version_context'; +import { useRuleCustomizationsContext } from '../../../../rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context'; const showBulkDuplicateExceptionsConfirmation = () => Promise.resolve(null); const showManualRuleRunConfirmation = () => Promise.resolve(null); @@ -23,7 +23,7 @@ jest.mock('../../../../../common/hooks/use_experimental_features'); jest.mock('../../../../rule_management/logic/bulk_actions/use_execute_bulk_action'); jest.mock('../../../../rule_management/logic/bulk_actions/use_bulk_export'); jest.mock( - '../../../../rule_management/components/rule_details/base_version_diff/base_version_context' + '../../../../rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context' ); const mockReportEvent = jest.fn(); @@ -51,11 +51,11 @@ jest.mock('../../../../../common/lib/kibana', () => { const useExecuteBulkActionMock = useExecuteBulkAction as jest.Mock; const useBulkExportMock = useBulkExport as jest.Mock; -const usePrebuiltRuleBaseVersionContextMock = usePrebuiltRuleBaseVersionContext as jest.Mock; +const useRuleCustomizationsContextMock = useRuleCustomizationsContext as jest.Mock; describe('RuleActionsOverflow', () => { beforeEach(() => { - usePrebuiltRuleBaseVersionContextMock.mockReturnValue({ + useRuleCustomizationsContextMock.mockReturnValue({ actions: { openCustomizationsRevertFlyout: jest.fn() }, state: { doesBaseVersionExist: true }, }); @@ -322,7 +322,7 @@ describe('RuleActionsOverflow', () => { }); test('it disabled the revert action when isRevertBaseVersionDisabled is true', async () => { - usePrebuiltRuleBaseVersionContextMock.mockReturnValue({ + useRuleCustomizationsContextMock.mockReturnValue({ actions: { openCustomizationsRevertFlyout: jest.fn() }, state: { doesBaseVersionExist: false }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx index 3e010c38ffefd..fb88dd446512f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/rule_actions_overflow/index.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { usePrebuiltRuleBaseVersionContext } from '../../../../rule_management/components/rule_details/base_version_diff/base_version_context'; +import { useRuleCustomizationsContext } from '../../../../rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context'; import { isCustomizedPrebuiltRule } from '../../../../../../common/api/detection_engine'; import { useScheduleRuleRun } from '../../../../rule_gaps/logic/use_schedule_rule_run'; import type { TimeRange } from '../../../../rule_gaps/types'; @@ -91,7 +91,7 @@ const RuleActionsOverflowComponent = ({ const { actions: { openCustomizationsRevertFlyout }, state: { doesBaseVersionExist }, - } = usePrebuiltRuleBaseVersionContext(); + } = useRuleCustomizationsContext(); const actions = useMemo( () => diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_context.tsx deleted file mode 100644 index de133b3579c64..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_context.tsx +++ /dev/null @@ -1,92 +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 { Dispatch, SetStateAction } from 'react'; -import React, { createContext, useContext, useMemo, useState } from 'react'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; -import { invariant } from '../../../../../../common/utils/invariant'; -import { usePrebuiltRulesViewBaseDiff } from './use_prebuilt_rules_view_base_diff'; - -export interface PrebuiltRuleBaseVersionState { - doesBaseVersionExist: boolean; - isLoading: boolean; - modifiedFields: Set; -} - -export interface PrebuiltRuleBaseVersionActions { - openCustomizationsPreviewFlyout: () => void; - openCustomizationsRevertFlyout: () => void; - setBaseVersionRule: Dispatch>; -} - -export interface PrebuiltRuleBaseVersionContextType { - state: PrebuiltRuleBaseVersionState; - actions: PrebuiltRuleBaseVersionActions; -} - -const PrebuiltRuleBaseVersionContext = createContext( - null -); - -interface PrebuiltRuleBaseVersionFlyoutContextProviderProps { - children: React.ReactNode; -} - -export const PrebuiltRuleBaseVersionFlyoutContextProvider = ({ - children, -}: PrebuiltRuleBaseVersionFlyoutContextProviderProps) => { - const [rule, setRule] = useState(null); - - const { - baseVersionFlyout, - openCustomizationsPreviewFlyout, - openCustomizationsRevertFlyout, - doesBaseVersionExist, - isLoading, - modifiedFields, - } = usePrebuiltRulesViewBaseDiff({ rule }); - - const actions = useMemo( - () => ({ - openCustomizationsPreviewFlyout, - openCustomizationsRevertFlyout, - setBaseVersionRule: setRule, - }), - [openCustomizationsPreviewFlyout, openCustomizationsRevertFlyout] - ); - - const providerValue = useMemo( - () => ({ - state: { - isLoading, - doesBaseVersionExist, - modifiedFields, - }, - actions, - }), - [actions, doesBaseVersionExist, isLoading, modifiedFields] - ); - - return ( - - <> - {children} - {baseVersionFlyout} - - - ); -}; - -export const usePrebuiltRuleBaseVersionContext = (): PrebuiltRuleBaseVersionContextType => { - const prebuiltRuleBaseVersionContext = useContext(PrebuiltRuleBaseVersionContext); - invariant( - prebuiltRuleBaseVersionContext, - 'usePrebuiltRuleBaseVersionContext should be used inside PrebuiltRuleBaseVersionFlyoutContextProvider' - ); - - return prebuiltRuleBaseVersionContext; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx index 3310886ce9d46..ebdaa636f8584 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/diff_components/field_diff.tsx @@ -16,6 +16,7 @@ import { DiffLayout, } from '../../../model/rule_details/rule_field_diff'; import { fieldToDisplayNameMap } from './translations'; +import { convertFieldToDisplayName } from '../helpers'; const SubFieldComponent = ({ currentVersion, @@ -33,7 +34,7 @@ const SubFieldComponent = ({ {shouldShowSubtitles ? ( -

{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}

+

{convertFieldToDisplayName(fieldName)}

) : null} {diffLayout === DiffLayout.RightToLeft ? ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts index dc78912a01e11..7d2e9cd56c5ca 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/get_humanized_field_name.ts @@ -42,6 +42,10 @@ export const getHumanizedFieldName = (fieldName: string) => { return i18n.MAX_SIGNALS_FIELD_LABEL; case 'tags': return i18n.TAGS_FIELD_LABEL; + case 'setup': + return i18n.SETUP_GUIDE_SECTION_LABEL; + case 'note': + return i18n.INVESTIGATION_GUIDE_TAB_LABEL; // Definition section fields case 'type': diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts index 3850ee98a84f6..d96169a7315a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPlainObject } from 'lodash'; +import { camelCase, isPlainObject, startCase } from 'lodash'; import type { Filter } from '@kbn/es-query'; import type { DiffableAllFields, @@ -22,6 +22,7 @@ import { } from './constants'; import * as i18n from './translations'; import { assertUnreachable } from '../../../../../common/utility_types'; +import { fieldToDisplayNameMap } from './diff_components/translations'; export const getSectionedFieldDiffs = (fields: FieldsGroupDiff[]) => { const aboutFields = []; @@ -115,3 +116,6 @@ export function getDataSourceProps(dataSource: DiffableAllFields['data_source']) return assertUnreachable(dataSource); } + +export const convertFieldToDisplayName = (fieldName: string) => + fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx index a63a96c32d2a5..2968cbf73a84b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_field_badge.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; import * as i18n from './translations'; -import { usePrebuiltRuleBaseVersionContext } from './base_version_diff/base_version_context'; +import { useRuleCustomizationsContext } from './rule_customizations_diff/rule_customizations_context'; import { PrebuiltRuleDiffBadge } from './prebuilt_rule_diff_badge'; interface ModifiedFieldBadgeProps { @@ -17,16 +18,22 @@ interface ModifiedFieldBadgeProps { export const ModifiedFieldBadge: React.FC = ({ fieldName }) => { const { state: { doesBaseVersionExist, modifiedFields }, - } = usePrebuiltRuleBaseVersionContext(); + } = useRuleCustomizationsContext(); if (!doesBaseVersionExist || !modifiedFields.has(fieldName)) { return null; } return ( - + + + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx index e60dccae06a2a..a3f5cce9973b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/modified_rule_badge.tsx @@ -6,11 +6,11 @@ */ import { EuiBadge, EuiToolTip } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { RuleResponse } from '../../../../../common/api/detection_engine'; import { isCustomizedPrebuiltRule } from '../../../../../common/api/detection_engine'; import * as i18n from './translations'; -import { usePrebuiltRuleBaseVersionContext } from './base_version_diff/base_version_context'; +import { useRuleCustomizationsContext } from './rule_customizations_diff/rule_customizations_context'; import { PrebuiltRuleDiffBadge } from './prebuilt_rule_diff_badge'; interface ModifiedRuleBadgeProps { @@ -20,25 +20,37 @@ interface ModifiedRuleBadgeProps { export const ModifiedRuleBadge: React.FC = ({ rule }) => { const { state: { doesBaseVersionExist }, - } = usePrebuiltRuleBaseVersionContext(); + } = useRuleCustomizationsContext(); + + const toolTipTitle = useMemo( + () => + doesBaseVersionExist + ? i18n.MODIFIED_PREBUILT_DIFF_TOOLTIP_TITLE + : i18n.NO_BASE_VERSION_MODIFIED_PREBUILT_DIFF_TOOLTIP_TITLE, + [doesBaseVersionExist] + ); + + const toolTipContent = useMemo( + () => + doesBaseVersionExist + ? i18n.MODIFIED_PREBUILT_DIFF_TOOLTIP_CONTENT + : i18n.NO_BASE_VERSION_MODIFIED_PREBUILT_DIFF_TOOLTIP_CONTENT, + [doesBaseVersionExist] + ); if (rule === null || !isCustomizedPrebuiltRule(rule)) { return null; } return ( - + {doesBaseVersionExist ? ( ) : ( - + {i18n.MODIFIED_PREBUILT_RULE_LABEL} )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx index 727e2ca0b5c00..be10409fc2d50 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/prebuilt_rule_diff_badge.tsx @@ -7,7 +7,7 @@ import { EuiBadge } from '@elastic/eui'; import React from 'react'; -import { usePrebuiltRuleBaseVersionContext } from './base_version_diff/base_version_context'; +import { useRuleCustomizationsContext } from './rule_customizations_diff/rule_customizations_context'; interface PrebuiltRuleDiffBadgeProps { label: string; @@ -17,7 +17,7 @@ interface PrebuiltRuleDiffBadgeProps { export const PrebuiltRuleDiffBadge = ({ label, dataTestSubj }: PrebuiltRuleDiffBadgeProps) => { const { actions: { openCustomizationsPreviewFlyout }, - } = usePrebuiltRuleBaseVersionContext(); + } = useRuleCustomizationsContext(); return ( {label} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx index 0207ea249176f..780c95c36a797 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx @@ -274,7 +274,6 @@ interface PrepareAboutSectionListItemsProps { rule: Partial; hideName?: boolean; hideDescription?: boolean; - showModifiedFields?: boolean; } // eslint-disable-next-line complexity @@ -282,7 +281,6 @@ const prepareAboutSectionListItems = ({ rule, hideName, hideDescription, - showModifiedFields = false, }: PrepareAboutSectionListItemsProps): EuiDescriptionListProps['listItems'] => { const aboutSectionListItems: EuiDescriptionListProps['listItems'] = []; @@ -311,7 +309,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -322,7 +320,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -338,10 +336,7 @@ const prepareAboutSectionListItems = ({ title: index === 0 ? ( - + ) : ( '' @@ -356,7 +351,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -372,10 +367,7 @@ const prepareAboutSectionListItems = ({ title: index === 0 ? ( - + ) : ( '' @@ -392,7 +384,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -403,7 +395,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -414,7 +406,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: ( @@ -434,7 +426,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -445,7 +437,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -456,10 +448,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -470,7 +459,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -481,7 +470,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -492,7 +481,7 @@ const prepareAboutSectionListItems = ({ aboutSectionListItems.push({ title: ( - + ), description: , @@ -507,7 +496,6 @@ export interface RuleAboutSectionProps extends React.ComponentProps { const aboutSectionListItems = prepareAboutSectionListItems({ rule, hideName, hideDescription, - showModifiedFields, }); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context.tsx new file mode 100644 index 0000000000000..09688dd837d5c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_context.tsx @@ -0,0 +1,87 @@ +/* + * 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, { createContext, useContext, useMemo } from 'react'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { useRuleCustomizationsDiff } from './use_rule_customizations_diff'; + +export interface RuleCustomizationsState { + doesBaseVersionExist: boolean; + isLoading: boolean; + modifiedFields: Set; +} + +export interface RuleCustomizationsActions { + openCustomizationsPreviewFlyout: () => void; + openCustomizationsRevertFlyout: () => void; +} + +export interface RuleCustomizationsContextType { + state: RuleCustomizationsState; + actions: RuleCustomizationsActions; +} + +export const RuleCustomizationsContext = createContext(null); + +interface RuleCustomizationsContextProviderProps { + rule: RuleResponse | null; + children: React.ReactNode; +} + +export const RuleCustomizationsContextProvider = ({ + rule, + children, +}: RuleCustomizationsContextProviderProps) => { + const { + ruleCustomizationsFlyout, + openCustomizationsPreviewFlyout, + openCustomizationsRevertFlyout, + doesBaseVersionExist, + isLoading, + modifiedFields, + } = useRuleCustomizationsDiff({ rule }); + + const actions = useMemo( + () => ({ + openCustomizationsPreviewFlyout, + openCustomizationsRevertFlyout, + }), + [openCustomizationsPreviewFlyout, openCustomizationsRevertFlyout] + ); + + const providerValue = useMemo( + () => ({ + state: { + isLoading, + doesBaseVersionExist, + modifiedFields, + }, + actions, + }), + [actions, doesBaseVersionExist, isLoading, modifiedFields] + ); + + return ( + + <> + {children} + {ruleCustomizationsFlyout} + + + ); +}; + +export const useRuleCustomizationsContext = (): RuleCustomizationsContextType => { + const ruleCustomizationsContext = useContext(RuleCustomizationsContext); + invariant( + ruleCustomizationsContext, + 'useRuleCustomizationsContext should be used inside RuleCustomizationsContextProvider' + ); + + return ruleCustomizationsContext; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout.test.tsx new file mode 100644 index 0000000000000..4e100d0098e22 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { render } from '@testing-library/react'; +import { RuleCustomizationsFlyout } from './rule_customizations_flyout'; +import { TestProviders } from '../../../../../common/mock'; +import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks'; +import { KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary'; + +jest.mock('../../../../../common/hooks/use_app_toasts', () => ({ + useAppToasts: jest.fn().mockReturnValue({ + addWarning: jest.fn(), + }), +})); + +describe('RuleCustomizationsFlyout', () => { + describe('concurrency control', () => { + it('shows a callout if data is stale', () => { + const currentRule = { ...getRulesSchemaMock(), name: 'current', revision: 1 }; + const baseRule = { ...getRulesSchemaMock(), name: 'base', revision: 1 }; + + const { rerender, queryByTestId } = render( + + + + + + ); + + expect(queryByTestId('ruleCustomizationsOutdatedCallout')).not.toBeInTheDocument(); + + rerender( + + + + + + ); + + expect(queryByTestId('ruleCustomizationsOutdatedCallout')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout.tsx similarity index 88% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout.tsx index 2c1c6dcee443a..cc66755a96089 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout.tsx @@ -14,20 +14,20 @@ import { RuleDetailsFlyout, TabContentPadding } from '../rule_details_flyout'; import * as ruleDetailsI18n from '../translations'; import * as i18n from './translations'; import { RuleDiffTab } from '../rule_diff_tab'; -import { BaseVersionDiffFlyoutSubheader } from './base_version_flyout_subheader'; +import { RuleCustomizationsFlyoutSubheader } from './rule_customizations_flyout_subheader'; import { getRevertRuleErrorStatusCode, useRevertPrebuiltRule, } from '../../../logic/prebuilt_rules/use_revert_prebuilt_rule'; import { DiffLayout } from '../../../model/rule_details/rule_field_diff'; -export const PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR = 'baseVersionPrebuiltRulePreview'; +export const RULE_CUSTOMIZATIONS_DIFF_FLYOUT_ANCHOR = 'ruleCustomizationsDiffFlyout'; interface PrebuiltRuleConcurrencyControl { revision: number; } -interface PrebuiltRulesBaseVersionFlyoutComponentProps { +interface RuleCustomizationsFlyoutProps { currentRule: RuleResponse; baseRule: RuleResponse; diff: PartialRuleDiff; @@ -35,19 +35,19 @@ interface PrebuiltRulesBaseVersionFlyoutComponentProps { isReverting: boolean; } -export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVersionFlyout({ +export const RuleCustomizationsFlyout = memo(function RuleCustomizationsFlyout({ currentRule, baseRule, diff, closeFlyout, isReverting, -}: PrebuiltRulesBaseVersionFlyoutComponentProps): JSX.Element { +}: RuleCustomizationsFlyoutProps): JSX.Element { const isOutdated = useConcurrencyControl(currentRule); const { mutateAsync: revertPrebuiltRule, isLoading } = useRevertPrebuiltRule(); const subHeader = useMemo( () => ( - - <>{i18n.BASE_VERSION_FLYOUT_UPDATES_TAB_TITLE} + + <>{i18n.RULE_CUSTOMIZATIONS_FLYOUT_UPDATES_TAB_TITLE} ), content: ( @@ -123,7 +123,7 @@ export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVer const jsonViewTab = { id: 'jsonViewUpdates', name: ( - + <>{ruleDetailsI18n.JSON_VIEW_UPDATES_TAB_LABEL} ), @@ -155,8 +155,8 @@ export const PrebuiltRulesBaseVersionFlyout = memo(function PrebuiltRulesBaseVer title={i18n.BASE_VERSION_FLYOUT_TITLE} rule={currentRule} size="l" - id={PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR} - dataTestSubj={PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR} + id={RULE_CUSTOMIZATIONS_DIFF_FLYOUT_ANCHOR} + dataTestSubj={RULE_CUSTOMIZATIONS_DIFF_FLYOUT_ANCHOR} closeFlyout={closeFlyout} ruleActions={ruleActions} extraTabs={extraTabs} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout_subheader.tsx similarity index 79% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout_subheader.tsx index 4eef7a4ec4ccc..d4afc0b863152 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/base_version_flyout_subheader.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/rule_customizations_flyout_subheader.tsx @@ -7,23 +7,22 @@ import React from 'react'; import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; -import { startCase, camelCase } from 'lodash'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import type { PartialRuleDiff, RuleResponse } from '../../../../../../common/api/detection_engine'; import * as i18n from './translations'; -import { fieldToDisplayNameMap } from '../diff_components/translations'; +import { convertFieldToDisplayName } from '../helpers'; -interface BaseVersionDiffFlyoutSubheaderProps { +interface RuleCustomizationsFlyoutSubheaderProps { currentRule: RuleResponse; diff: PartialRuleDiff; isOutdated: boolean; } -export const BaseVersionDiffFlyoutSubheader = ({ +export const RuleCustomizationsFlyoutSubheader = ({ currentRule, diff, isOutdated, -}: BaseVersionDiffFlyoutSubheaderProps) => { +}: RuleCustomizationsFlyoutSubheaderProps) => { const lastUpdate = ( @@ -44,9 +43,7 @@ export const BaseVersionDiffFlyoutSubheader = ({ {i18n.FIELD_MODIFICATIONS} {':'} {' '} - {fieldsDiff - .map((fieldName) => fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))) - .join(', ')} + {fieldsDiff.map((fieldName) => convertFieldToDisplayName(fieldName)).join(', ')} ); @@ -62,7 +59,11 @@ export const BaseVersionDiffFlyoutSubheader = ({ {isOutdated && ( <> - +

{i18n.OUTDATED_DIFF_CALLOUT_MESSAGE}

diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/translations.tsx similarity index 94% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/translations.tsx index 276a4bad81e87..8aeeada77412e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/translations.tsx @@ -60,21 +60,21 @@ export const REVERTING_RULE_CALLOUT_MESSAGE = i18n.translate( } ); -export const BASE_VERSION_FLYOUT_UPDATES_TAB_TITLE = i18n.translate( +export const RULE_CUSTOMIZATIONS_FLYOUT_UPDATES_TAB_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.baseVersionFlyout.updatesTabTitle', { defaultMessage: 'Elastic rule diff overview', } ); -export const BASE_VERSION_FLYOUT_UPDATES_TAB_TOOLTIP = i18n.translate( +export const RULE_CUSTOMIZATIONS_FLYOUT_UPDATES_TAB_TOOLTIP = i18n.translate( 'xpack.securitySolution.detectionEngine.baseVersionFlyout.updatesTabTooltip', { defaultMessage: 'See all changes made to the rule', } ); -export const BASE_VERSION_FLYOUT_JSON_TAB_TOOLTIP = i18n.translate( +export const RULE_CUSTOMIZATIONS_FLYOUT_JSON_TAB_TOOLTIP = i18n.translate( 'xpack.securitySolution.detectionEngine.baseVersionFlyout.jsonTabTooltip', { defaultMessage: 'See all changes made to the rule in JSON format', diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/use_prebuilt_rules_view_base_diff.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/use_rule_customizations_diff.tsx similarity index 85% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/use_prebuilt_rules_view_base_diff.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/use_rule_customizations_diff.tsx index 2fa47adf785cd..a8f1b03116a39 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/base_version_diff/use_prebuilt_rules_view_base_diff.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_customizations_diff/use_rule_customizations_diff.tsx @@ -10,15 +10,15 @@ import { useBoolean } from '@kbn/react-hooks'; import { isCustomizedPrebuiltRule } from '../../../../../../common/api/detection_engine/model/rule_schema/utils'; import type { RuleResponse } from '../../../../../../common/api/detection_engine'; import { useFetchPrebuiltRuleBaseVersionQuery } from '../../../api/hooks/prebuilt_rules/use_fetch_prebuilt_rule_base_version_query'; -import { PrebuiltRulesBaseVersionFlyout } from './base_version_flyout'; +import { RuleCustomizationsFlyout } from './rule_customizations_flyout'; -export const PREBUILT_RULE_BASE_VERSION_FLYOUT_ANCHOR = 'baseVersionPrebuiltRulePreview'; +export const PREBUILT_RULE_CUSTOMIZATIONS_FLYOUT_ANCHOR = 'PrebuiltRuleCustomizationsPreview'; -interface UsePrebuiltRulesViewBaseDiffProps { +interface UseRuleCustomizationsDiffProps { rule: RuleResponse | null; } -export const usePrebuiltRulesViewBaseDiff = ({ rule }: UsePrebuiltRulesViewBaseDiffProps) => { +export const useRuleCustomizationsDiff = ({ rule }: UseRuleCustomizationsDiffProps) => { const [isFlyoutOpen, { off: closeFlyout, on: openFlyout }] = useBoolean(false); const [isReverting, { off: setRevertingFalse, on: setRevertingTrue }] = useBoolean(false); @@ -47,9 +47,9 @@ export const usePrebuiltRulesViewBaseDiff = ({ rule }: UsePrebuiltRulesViewBaseD ); return { - baseVersionFlyout: + ruleCustomizationsFlyout: isFlyoutOpen && !isLoading && data != null && doesBaseVersionExist ? ( - { const definitionSectionListItems: EuiDescriptionListProps['listItems'] = []; @@ -507,11 +505,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -523,11 +517,7 @@ const prepareDefinitionSectionListItems = ({ { title: ( - + ), description: , @@ -538,7 +528,6 @@ const prepareDefinitionSectionListItems = ({ ), @@ -555,7 +544,6 @@ const prepareDefinitionSectionListItems = ({ ), @@ -564,11 +552,7 @@ const prepareDefinitionSectionListItems = ({ { title: ( - + ), description: ( @@ -586,7 +570,6 @@ const prepareDefinitionSectionListItems = ({ ), @@ -605,11 +588,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -626,11 +605,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -649,11 +624,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -662,11 +633,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -676,11 +643,7 @@ const prepareDefinitionSectionListItems = ({ { title: ( - + ), description: , @@ -688,11 +651,7 @@ const prepareDefinitionSectionListItems = ({ { title: ( - + ), description: ( @@ -709,11 +668,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -728,11 +683,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -747,11 +698,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -764,7 +711,7 @@ const prepareDefinitionSectionListItems = ({ if (rule.type) { definitionSectionListItems.push({ - title: , + title: , description: , }); } @@ -773,7 +720,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -784,10 +731,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -803,7 +747,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -819,7 +763,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -829,7 +773,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -841,7 +785,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -852,7 +796,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -863,11 +807,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -885,11 +825,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -900,11 +836,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: ( @@ -919,7 +851,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -930,7 +862,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -941,7 +873,7 @@ const prepareDefinitionSectionListItems = ({ definitionSectionListItems.push({ title: ( - + ), description: , @@ -1003,7 +935,6 @@ export interface RuleDefinitionSectionProps columnWidths?: EuiDescriptionListProps['columnWidths']; isInteractive?: boolean; dataTestSubj?: string; - showModifiedFields?: boolean; } export const RuleDefinitionSection = ({ @@ -1011,7 +942,6 @@ export const RuleDefinitionSection = ({ isInteractive = false, columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, dataTestSubj, - showModifiedFields, ...descriptionListProps }: RuleDefinitionSectionProps) => { const { savedQuery } = useGetSavedQuery({ @@ -1026,7 +956,6 @@ export const RuleDefinitionSection = ({ isInteractive, savedQuery, isSuppressionEnabled, - showModifiedFields, }); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx index 773d5e6c642fb..36c75c4ea646a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_field_name.tsx @@ -6,23 +6,20 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import React from 'react'; +import React, { useContext } from 'react'; import { ModifiedFieldBadge } from './modified_field_badge'; import { getHumanizedFieldName } from './get_humanized_field_name'; +import { RuleCustomizationsContext } from './rule_customizations_diff/rule_customizations_context'; interface RuleFieldNameProps { fieldName: string; label?: string; - showModifiedFields?: boolean; } -export const RuleFieldName = ({ - fieldName, - label, - showModifiedFields = false, -}: RuleFieldNameProps) => { +export const RuleFieldName = ({ fieldName, label }: RuleFieldNameProps) => { + const ruleCustomizationsContext = useContext(RuleCustomizationsContext); const humanizedFieldName = getHumanizedFieldName(fieldName); - return showModifiedFields ? ( + return ruleCustomizationsContext != null ? ( {label ?? humanizedFieldName} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx index 2ac87a225b76e..d2a0691848a76 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx @@ -49,13 +49,11 @@ const LookBack = ({ value }: LookBackProps) => ( export interface RuleScheduleSectionProps extends React.ComponentProps { rule: Partial; columnWidths?: EuiDescriptionListProps['columnWidths']; - showModifiedFields?: boolean; } export const RuleScheduleSection = ({ rule, columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS, - showModifiedFields = false, ...descriptionListProps }: RuleScheduleSectionProps) => { if (!rule.interval || !rule.from) { @@ -75,11 +73,7 @@ export const RuleScheduleSection = ({ { title: ( - + ), description: , @@ -90,7 +84,6 @@ export const RuleScheduleSection = ({ ), @@ -108,11 +101,7 @@ export const RuleScheduleSection = ({ { title: ( - + ), description: , @@ -120,11 +109,7 @@ export const RuleScheduleSection = ({ { title: ( - + ), description: , diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_header.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_header.tsx index 91339e25d5907..71e04e72c5038 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_header.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_header.tsx @@ -6,13 +6,12 @@ */ import React from 'react'; -import { camelCase, startCase } from 'lodash'; import { EuiFlexGroup, EuiTitle } from '@elastic/eui'; -import { fieldToDisplayNameMap } from '../../diff_components/translations'; import type { FieldUpgradeStateEnum } from '../../../../model/prebuilt_rule_upgrade'; import { FieldUpgradeStateInfo } from './field_upgrade_state_info'; import { ModifiedBadge } from '../badges/modified_badge'; import { FIELD_MODIFIED_BADGE_DESCRIPTION } from './translations'; +import { convertFieldToDisplayName } from '../../helpers'; interface FieldUpgradeHeaderProps { fieldName: string; @@ -28,7 +27,7 @@ export function FieldUpgradeHeader({ return ( -
{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}
+
{convertFieldToDisplayName(fieldName)}
{isCustomized && } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx index 2d4d718a67aca..a67296916eb95 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/translations.tsx @@ -434,6 +434,14 @@ export const MODIFIED_PREBUILT_RULE_PER_FIELD_LABEL = i18n.translate( } ); +export const MODIFIED_FIELD_BADGE_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.customizedPrebuiltRulePerFieldTooltipContent', + { + defaultMessage: + 'This field has been modified from the original Elastic version. Click to open.', + } +); + export const QUERY_LANGUAGE_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.queryLanguageLabel', { @@ -486,12 +494,26 @@ export const HAS_RULE_UPDATE_CALLOUT_BUTTON = i18n.translate( export const MODIFIED_PREBUILT_DIFF_TOOLTIP_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.ruleDiffTooltipTitle', { - defaultMessage: 'Unable to view rule diff', + defaultMessage: 'Modified', } ); export const MODIFIED_PREBUILT_DIFF_TOOLTIP_CONTENT = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.ruleDiffTooltipContent', + { + defaultMessage: 'This rule has been modified from the original Elastic version. Click to open.', + } +); + +export const NO_BASE_VERSION_MODIFIED_PREBUILT_DIFF_TOOLTIP_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.noBaseVersionRuleDiffTooltipTitle', + { + defaultMessage: 'Unable to view rule diff', + } +); + +export const NO_BASE_VERSION_MODIFIED_PREBUILT_DIFF_TOOLTIP_CONTENT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.noBaseVersionRuleDiffTooltipContent', { defaultMessage: "This rule hasn't been updated in a while and the original Elastic version cannot be found. We recommend updating this rule to the latest version.", diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader.tsx index bab43e55c2e00..e4868fff6913d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_flyout_subheader.tsx @@ -7,13 +7,12 @@ import React, { memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; -import { camelCase, startCase } from 'lodash'; +import { convertFieldToDisplayName } from '../../../../rule_management/components/rule_details/helpers'; import type { FieldsDiff } from '../../../../../../common/api/detection_engine'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import { SeverityBadge } from '../../../../../common/components/severity_badge'; import { ModifiedBadge } from '../../../../rule_management/components/rule_details/three_way_diff/badges/modified_badge'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { fieldToDisplayNameMap } from '../../../../rule_management/components/rule_details/diff_components/translations'; import * as i18n from './translations'; interface UpgradeFlyoutSubHeaderProps { @@ -63,9 +62,7 @@ export const UpgradeFlyoutSubHeader = memo(function UpgradeFlyoutSubHeader({ {i18n.FIELD_UPDATES} {':'} {' '} - {fieldsNamesWithUpdates - .map((fieldName) => fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))) - .join(', ')} + {fieldsNamesWithUpdates.map((fieldName) => convertFieldToDisplayName(fieldName)).join(', ')}
); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts index 4f5f18f2d6128..4bbd743f649fa 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/common/revert_prebuilt_rules/revert_prebuilt_rules.ts @@ -6,7 +6,11 @@ */ import expect from 'expect'; import { BulkRevertSkipReasonEnum } from '@kbn/security-solution-plugin/common/api/detection_engine'; -import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + deleteAllRules, + waitForRulePartialFailure, +} from '../../../../../../../common/utils/security_solution'; import { FtrProviderContext } from '../../../../../../ftr_provider_context'; import { createPrebuiltRuleAssetSavedObjects, @@ -90,86 +94,159 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('does not modify `exception_list` field', async () => { - const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ - body: { - rule_id: 'rule_1', - description: 'new description', - exceptions_list: [ - { - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', - }, - ], - }, - }); + describe('customization adjacent fields', () => { + it('does not modify `exception_list` field', async () => { + const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ + body: { + rule_id: 'rule_1', + description: 'new description', + exceptions_list: [ + { + id: 'some_uuid', + list_id: 'list_id_single', + namespace_type: 'single', + type: 'detection', + }, + ], + }, + }); - const response = await revertPrebuiltRule(supertest, { - id: customizedPrebuiltRule.id, - version: customizedPrebuiltRule.version, - revision: customizedPrebuiltRule.revision, + const response = await revertPrebuiltRule(supertest, { + id: customizedPrebuiltRule.id, + version: customizedPrebuiltRule.version, + revision: customizedPrebuiltRule.revision, + }); + + expect(response.attributes.results.updated).toEqual([ + expect.objectContaining({ + rule_source: { + is_customized: false, + type: 'external', + }, + exceptions_list: [ + expect.objectContaining({ + id: 'some_uuid', + list_id: 'list_id_single', + namespace_type: 'single', + type: 'detection', + }), + ], + }), + ]); }); - expect(response.attributes.results.updated).toEqual([ - expect.objectContaining({ - rule_source: { - is_customized: false, - type: 'external', + it('does not modify `actions` field', async () => { + const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ + body: { + rule_id: 'rule_1', + description: 'new description', + actions: [ + // use a pre-configured connector + { + group: 'default', + id: 'my-test-email', + action_type_id: '.email', + params: {}, + }, + ], }, - exceptions_list: [ - expect.objectContaining({ - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', - }), - ], - }), - ]); - }); + }); - it('does not modify `actions` field', async () => { - const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ - body: { - rule_id: 'rule_1', - description: 'new description', - actions: [ - // use a pre-configured connector - { - group: 'default', - id: 'my-test-email', - action_type_id: '.email', - params: {}, + const response = await revertPrebuiltRule(supertest, { + id: customizedPrebuiltRule.id, + version: customizedPrebuiltRule.version, + revision: customizedPrebuiltRule.revision, + }); + + expect(response.attributes.results.updated).toEqual([ + expect.objectContaining({ + rule_source: { + is_customized: false, + type: 'external', }, - ], - }, + actions: [ + expect.objectContaining({ + id: 'my-test-email', + action_type_id: '.email', + frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, + group: 'default', + params: {}, + }), + ], + }), + ]); }); - const response = await revertPrebuiltRule(supertest, { - id: customizedPrebuiltRule.id, - version: customizedPrebuiltRule.version, - revision: customizedPrebuiltRule.revision, + it('does not modify `execution_summary` field', async () => { + const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ + body: { + rule_id: 'rule_1', + description: 'new description', + enabled: true, + }, + }); + + await waitForRulePartialFailure({ supertest, log, id: customizedPrebuiltRule.id }); + + // Get the original execution summary field + const { body } = await supertest + .get(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .query({ id: customizedPrebuiltRule.id }) + .expect(200); + + // Revert the rule + await revertPrebuiltRule(supertest, { + id: customizedPrebuiltRule.id, + version: customizedPrebuiltRule.version, + revision: customizedPrebuiltRule.revision, + }); + + // Get the reverted rule's execution summary field + const { body: updatedBody } = await supertest + .get(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .query({ id: customizedPrebuiltRule.id }) + .expect(200); + + expect(updatedBody).toEqual( + expect.objectContaining({ + rule_source: { + is_customized: false, + type: 'external', + }, + execution_summary: body.execution_summary, + }) + ); }); - expect(response.attributes.results.updated).toEqual([ - expect.objectContaining({ - rule_source: { - is_customized: false, - type: 'external', + it('does not modify `enabled` field', async () => { + const { body: customizedPrebuiltRule } = await securitySolutionApi.patchRule({ + body: { + rule_id: 'rule_1', + description: 'new description', + enabled: true, }, - actions: [ - expect.objectContaining({ - id: 'my-test-email', - action_type_id: '.email', - frequency: { notifyWhen: 'onActiveAlert', summary: true, throttle: null }, - group: 'default', - params: {}, - }), - ], - }), - ]); + }); + + const response = await revertPrebuiltRule(supertest, { + id: customizedPrebuiltRule.id, + version: customizedPrebuiltRule.version, + revision: customizedPrebuiltRule.revision, + }); + + expect(response.attributes.results.updated).toEqual([ + expect.objectContaining({ + rule_source: { + is_customized: false, + type: 'external', + }, + enabled: true, + }), + ]); + }); }); it("skips a prebuilt rule if it's not customized", async () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/revert_prebuilt_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/revert_prebuilt_rule.cy.ts index 4b6c0bae6e5d7..ae190e30e7522 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/revert_prebuilt_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/revert_prebuilt_rule.cy.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { BASE_VERSION_FLYOUT } from '../../../../screens/rule_details_flyout'; +import { RULE_CUSTOMIZATIONS_DIFF_FLYOUT } from '../../../../screens/rule_details_flyout'; import { RULE_UPGRADE_PER_FIELD_DIFF_LABEL } from '../../../../screens/rule_updates'; import { POPOVER_ACTIONS_TRIGGER_BUTTON, RULE_NAME_HEADER } from '../../../../screens/rule_details'; import { getIndexPatterns, getNewRule } from '../../../../objects/rule'; import { - expectModifiedBadgeToNotBeDisplayed, + expectModifiedRuleBadgeToNotBeDisplayed, revertRuleFromDetailsPage, } from '../../../../tasks/alerts_detection_rules'; import { @@ -63,7 +63,7 @@ describe( cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); revertRuleFromDetailsPage(); - expectModifiedBadgeToNotBeDisplayed(); + expectModifiedRuleBadgeToNotBeDisplayed(); cy.get(RULE_NAME_HEADER).should('contain', 'Non-customized prebuilt rule'); // Correctly displays reverted title }); @@ -88,7 +88,7 @@ describe( patchRule('rule_1', { description: 'customized description' }); // Customize another field to iterate revision field revertRuleFromDetailsPage(); - cy.get(BASE_VERSION_FLYOUT).should('exist'); // Flyout shouldn't be closed + cy.get(RULE_CUSTOMIZATIONS_DIFF_FLYOUT).should('exist'); // Flyout shouldn't be closed cy.get(TOASTER_MESSAGE).should( 'have.text', 'Something in the rule object has changed before reversion was completed. Please review the updated diff and try again.' diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/rule_customization.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/rule_customization.cy.ts index ff8dd8ffd12d6..2a584026b120b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/rule_customization.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/rule_customization.cy.ts @@ -30,13 +30,21 @@ import { DEFINITION_EDIT_TAB, SCHEDULE_EDIT_TAB, } from '../../../../screens/create_new_rule'; -import { ABOUT_RULE_DESCRIPTION } from '../../../../screens/rule_details'; +import { + ABOUT_RULE_DESCRIPTION, + MODIFIED_PREBUILT_RULE_BADGE, + MODIFIED_PREBUILT_RULE_BADGE_NO_BASE_VERSION, + MODIFIED_PREBUILT_RULE_PER_FIELD_BADGE, + RULE_CUSTOMIZATIONS_DIFF_FLYOUT, +} from '../../../../screens/rule_details'; import { goToRuleEditSettings } from '../../../../tasks/rule_details'; import { getIndexPatterns, getNewRule } from '../../../../objects/rule'; import { editFirstRule, - expectModifiedBadgeToBeDisplayed, - expectModifiedBadgeToNotBeDisplayed, + expectModifiedRuleBadgeToBeDisplayed, + expectModifiedRuleBadgeToNotBeDisplayed, + expectModifiedRulePerFieldBadgeToBeDisplayed, + expectModifiedRulePerFieldBadgeToNotBeDisplayed, expectToContainModifiedBadge, expectToNotContainModifiedBadge, filterByCustomRules, @@ -116,7 +124,7 @@ describe( fillDescription(newDescriptionValue); saveEditedRule(); - expectModifiedBadgeToBeDisplayed(); + expectModifiedRuleBadgeToBeDisplayed(); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newDescriptionValue); }); @@ -126,14 +134,14 @@ describe( visitRulesManagementTable(); cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); - expectModifiedBadgeToBeDisplayed(); // Expect modified badge to already be displayed + expectModifiedRuleBadgeToBeDisplayed(); // Expect modified badge to already be displayed goToRuleEditSettings(); goToAboutStepTab(); fillDescription(newDescriptionValue); saveEditedRule(); - expectModifiedBadgeToBeDisplayed(); + expectModifiedRuleBadgeToBeDisplayed(); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newDescriptionValue); }); @@ -261,77 +269,125 @@ describe( }); describe('calculating the Modified badge', () => { - it('modified badge should appear on the rule details page when prebuilt rule is customized', function () { - patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule - visitRulesManagementTable(); + describe('on the rule details page', () => { + it('should open the rule diff flyout on click when rule is customized', function () { + patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule + visitRulesManagementTable(); + + cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); + expectModifiedRuleBadgeToBeDisplayed(); // Expect modified badge to be displayed + cy.get(MODIFIED_PREBUILT_RULE_BADGE).click(); + cy.get(RULE_CUSTOMIZATIONS_DIFF_FLYOUT).should('exist'); + }); - cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); - expectModifiedBadgeToBeDisplayed(); // Expect modified badge to be displayed - }); + it('should not open the rule diff flyout on click when rule is customized but base version does not exist', function () { + patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule + deletePrebuiltRulesAssets(); + visitRulesManagementTable(); - it("modified badge should not appear on the rule details page when prebuilt rule isn't customized", function () { - visitRulesManagementTable(); + cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); + cy.get(MODIFIED_PREBUILT_RULE_BADGE_NO_BASE_VERSION).should('exist'); // Expect modified badge to be displayed + cy.get(MODIFIED_PREBUILT_RULE_BADGE_NO_BASE_VERSION).click(); + cy.get(RULE_CUSTOMIZATIONS_DIFF_FLYOUT).should('not.exist'); + }); - cy.get(RULE_NAME).contains('Non-customized prebuilt rule').click(); - expectModifiedBadgeToNotBeDisplayed(); // Expect modified badge to not be displayed - }); + it("should not be displayed when rule isn't customized", function () { + visitRulesManagementTable(); - it("modified badge should not appear on a custom rule's rule details page", function () { - visitRulesManagementTable(); + cy.get(RULE_NAME).contains('Non-customized prebuilt rule').click(); + expectModifiedRuleBadgeToNotBeDisplayed(); // Expect modified badge to not be displayed + }); - cy.get(RULE_NAME).contains('Custom rule').click(); - expectModifiedBadgeToNotBeDisplayed(); // Expect modified badge to not be displayed + it('should not be displayed when rule is not prebuilt', function () { + visitRulesManagementTable(); + + cy.get(RULE_NAME).contains('Custom rule').click(); + expectModifiedRuleBadgeToNotBeDisplayed(); // Expect modified badge to not be displayed + }); }); - it('modified badge should appear on the rule management table when prebuilt rule is modified', function () { - patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule - visitRulesManagementTable(); + describe('on the rule management table', () => { + it('should be displayed in row when prebuilt rule is customized', function () { + patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule + visitRulesManagementTable(); - filterByElasticRules(); - expectToContainModifiedBadge('Customized prebuilt rule'); + filterByElasticRules(); + expectToContainModifiedBadge('Customized prebuilt rule'); + }); + + it("should not be displayed in row when prebuilt rule isn't customized", function () { + visitRulesManagementTable(); + + filterByElasticRules(); + expectToNotContainModifiedBadge('Non-customized prebuilt rule'); + }); + + it('should not be displayed in row when rule is not prebuilt', function () { + visitRulesManagementTable(); + + filterByCustomRules(); + expectToNotContainModifiedBadge('Custom rule'); + }); }); - it("modified badge should not appear on the rule management table when prebuilt rule isn't customized", function () { - visitRulesManagementTable(); + describe('on the rule updates table', () => { + it('should be displayed when prebuilt rule is customized', function () { + // Create a new version of the rule to trigger the rule update logic + installPrebuiltRuleAssets([ + { + ...PREBUILT_RULE, + 'security-rule': { ...PREBUILT_RULE['security-rule'], version: 2 }, + }, + ]); + patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule + visitRulesManagementTable(); + clickRuleUpdatesTab(); + + cy.get(MODIFIED_RULE_BADGE).should('exist'); + }); - filterByElasticRules(); - expectToNotContainModifiedBadge('Non-customized prebuilt rule'); + it("should not be displayed when prebuilt rule isn't customized", function () { + // Create a new version of the rule to trigger the rule update logic + installPrebuiltRuleAssets([ + { + ...PREBUILT_RULE, + 'security-rule': { ...PREBUILT_RULE['security-rule'], version: 2 }, + }, + ]); + visitRulesManagementTable(); + clickRuleUpdatesTab(); + + cy.get(MODIFIED_RULE_BADGE).should('not.exist'); + }); }); + }); - it('modified badge should not appear on the rule management table when row is a custom rule', function () { + describe('calculating the per-field modified badge', () => { + it('should appear next to fields that have been customized', function () { + patchRule('rule_1', { name: 'Customized prebuilt rule', tags: ['test'] }); // We want to make this a customized prebuilt rule visitRulesManagementTable(); - filterByCustomRules(); - expectToNotContainModifiedBadge('Custom rule'); + cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); + expectModifiedRulePerFieldBadgeToBeDisplayed('tags'); // Customized fields should have a badge present + expectModifiedRulePerFieldBadgeToNotBeDisplayed('max_signals'); // Non-customized fields should not have a badge present }); - it('modified badge should appear on the rule updates table when prebuilt rule is customized', function () { - // Create a new version of the rule to trigger the rule update logic - installPrebuiltRuleAssets([ - { - ...PREBUILT_RULE, - 'security-rule': { ...PREBUILT_RULE['security-rule'], version: 2 }, - }, - ]); - patchRule('rule_1', { name: 'Customized prebuilt rule' }); // We want to make this a customized prebuilt rule + it('should open the rule customizations diff flyout on click', function () { + patchRule('rule_1', { name: 'Customized prebuilt rule', tags: ['test'] }); // We want to make this a customized prebuilt rule visitRulesManagementTable(); - clickRuleUpdatesTab(); - cy.get(MODIFIED_RULE_BADGE).should('exist'); + cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); + cy.get(MODIFIED_PREBUILT_RULE_PER_FIELD_BADGE('tags')).click(); + cy.get(RULE_CUSTOMIZATIONS_DIFF_FLYOUT).should('exist'); }); - it("Modified badge should not appear on the rule updates table when prebuilt rule isn't customized", function () { - // Create a new version of the rule to trigger the rule update logic - installPrebuiltRuleAssets([ - { - ...PREBUILT_RULE, - 'security-rule': { ...PREBUILT_RULE['security-rule'], version: 2 }, - }, - ]); + it('should not be displayed when the rule base version does not exist', function () { + patchRule('rule_1', { name: 'Customized prebuilt rule', tags: ['test'] }); // We want to make this a customized prebuilt rule + deletePrebuiltRulesAssets(); visitRulesManagementTable(); - clickRuleUpdatesTab(); - cy.get(MODIFIED_RULE_BADGE).should('not.exist'); + cy.get(RULE_NAME).contains('Customized prebuilt rule').click(); + expectModifiedRulePerFieldBadgeToNotBeDisplayed('tags'); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts index c55046b72564c..f91511f7d5e2d 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts @@ -205,3 +205,8 @@ export const RULE_GAPS_DATE_PICKER = '[data-test-subj="rule-gaps-date-picker"]'; export const RULE_GAPS_DATE_PICKER_APPLY_REFRESH = `${RULE_GAPS_DATE_PICKER} .euiSuperUpdateButton`; export const RULE_FILL_ALL_GAPS_BUTTON = '[data-test-subj="fill-rule-gaps-button"]'; export const MODIFIED_PREBUILT_RULE_BADGE = '[data-test-subj="modified-prebuilt-rule-badge"]'; +export const MODIFIED_PREBUILT_RULE_BADGE_NO_BASE_VERSION = + '[data-test-subj="modified-prebuilt-rule-badge-no-base-version"]'; +export const MODIFIED_PREBUILT_RULE_PER_FIELD_BADGE = (fieldName: string) => + `[data-test-subj="modified-prebuilt-rule-per-field-${fieldName}-badge"]`; +export const RULE_CUSTOMIZATIONS_DIFF_FLYOUT = '[data-test-subj="ruleCustomizationsDiffFlyout"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.ts index 8d83c23f2ecc2..7bcfa6cde9a10 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details_flyout.ts @@ -10,4 +10,4 @@ export const TABLE_TAB = '[data-test-subj="securitySolutionDocumentDetailsFlyout export const FILTER_INPUT = '[data-test-subj="securitySolutionDocumentDetailsFlyoutBody"] [data-test-subj="search-input"]'; -export const BASE_VERSION_FLYOUT = '[data-test-subj="baseVersionPrebuiltRulePreview"]'; +export const RULE_CUSTOMIZATIONS_DIFF_FLYOUT = '[data-test-subj="ruleCustomizationsDiffFlyout"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts index 7afe0c49acdf0..f01e5ef0d1548 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts @@ -63,6 +63,7 @@ import type { RULES_MONITORING_TABLE } from '../screens/alerts_detection_rules'; import { EUI_CHECKBOX } from '../screens/common/controls'; import { MODIFIED_PREBUILT_RULE_BADGE, + MODIFIED_PREBUILT_RULE_PER_FIELD_BADGE, POPOVER_ACTIONS_TRIGGER_BUTTON, RULE_NAME_HEADER, } from '../screens/rule_details'; @@ -408,14 +409,22 @@ export const expectToContainRule = ( cy.get(tableSelector).find(RULES_ROW).should('include.text', ruleName); }; -export const expectModifiedBadgeToBeDisplayed = () => { +export const expectModifiedRuleBadgeToBeDisplayed = () => { cy.get(MODIFIED_PREBUILT_RULE_BADGE).should('exist'); }; -export const expectModifiedBadgeToNotBeDisplayed = () => { +export const expectModifiedRulePerFieldBadgeToBeDisplayed = (fieldName: string) => { + cy.get(MODIFIED_PREBUILT_RULE_PER_FIELD_BADGE(fieldName)).should('exist'); +}; + +export const expectModifiedRuleBadgeToNotBeDisplayed = () => { cy.get(MODIFIED_PREBUILT_RULE_BADGE).should('not.exist'); }; +export const expectModifiedRulePerFieldBadgeToNotBeDisplayed = (fieldName: string) => { + cy.get(MODIFIED_PREBUILT_RULE_PER_FIELD_BADGE(fieldName)).should('not.exist'); +}; + const selectOverwriteRulesImport = () => { cy.get(RULE_IMPORT_OVERWRITE_CHECKBOX).check({ force: true }); cy.get(RULE_IMPORT_OVERWRITE_CHECKBOX).should('be.checked');