diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 9907aea709e58..bcabd1325ed51 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -16,6 +16,14 @@ import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '.. import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { TableId } from '@kbn/securitysolution-data-table'; import { TimelineId } from '../../../../../common/types/timeline'; +import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config'; + +jest.mock('../../../containers/detection_engine/lists/use_lists_config'); + +const mockUseHasSecurityCapability = jest.fn().mockReturnValue(false); +jest.mock('../../../../helper_hooks', () => ({ + useHasSecurityCapability: () => mockUseHasSecurityCapability(), +})); jest.mock('../../../../common/components/user_privileges'); @@ -98,6 +106,14 @@ const openAlertDetailsPageButton = '[data-test-subj="open-alert-details-page-men const applyAlertTagsButton = '[data-test-subj="alert-tags-context-menu-item"]'; describe('Alert table context menu', () => { + beforeEach(() => { + (useListsConfig as jest.Mock).mockImplementation(() => ({ + loading: false, + needsConfiguration: false, + })); + mockUseHasSecurityCapability.mockReturnValue(true); + }); + describe('Case actions', () => { test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => { const wrapper = mount(, { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index c2d477d5a87c6..398b2dc524c20 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -35,7 +35,7 @@ import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_ import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/event_filters_flyout'; import { useAlertsActions } from './use_alerts_actions'; import { useExceptionFlyout } from './use_add_exception_flyout'; -import { useExceptionActions } from './use_add_exception_actions'; +import { useAlertExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; import type { Status } from '../../../../../common/api/detection_engine'; import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations'; @@ -195,7 +195,7 @@ const AlertContextMenuComponent: React.FC { + const { exceptionActionItems } = useExceptionActions({ + isEndpointAlert, + onAddExceptionTypeClick, + }); + + const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = + useListsConfig(); + const canReadEndpointExceptions = useHasSecurityCapability('writeEndpointExceptions'); + + const canAccessRuleExceptions = useMemo( + () => !listsConfigLoading && !needsListsConfiguration && canReadEndpointExceptions, + [canReadEndpointExceptions, listsConfigLoading, needsListsConfiguration] + ); + // Endpoint exceptions are available for: + // Serverless Endpoint Essentials/Complete PLI and + // on ESS Security Kibana sub-feature Endpoint Exceptions (enabled when Security feature is enabled) + if (canAccessRuleExceptions) { + return { exceptionActionItems }; + } + + return { + exceptionActionItems: exceptionActionItems.map((item) => { + if (item.name === ACTION_ADD_ENDPOINT_EXCEPTION) { + item.disabled = true; + } + return item; + }), + }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index d342092dd3d9d..9fb4ea3e17d3d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -13,12 +13,12 @@ import type { TakeActionDropdownProps } from '.'; import { TakeActionDropdown } from '.'; import { generateAlertDetailsDataMock } from '../../../common/components/event_details/__mocks__'; import { getDetectionAlertMock } from '../../../common/mock/mock_detection_alerts'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { TimelineId } from '../../../../common/types/timeline'; import { TestProviders } from '../../../common/mock'; import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { useKibana, useGetUserCasesPermissions, useHttp } from '../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useHttp, useKibana } from '../../../common/lib/kibana'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../common/components/user_privileges/user_privileges_context'; import { useUserPrivileges } from '../../../common/components/user_privileges'; @@ -30,15 +30,17 @@ import { import { endpointMetadataHttpMocks } from '../../../management/pages/endpoint_hosts/mocks'; import type { HttpSetup } from '@kbn/core/public'; import { - isAlertFromEndpointEvent, isAlertFromEndpointAlert, + isAlertFromEndpointEvent, } from '../../../common/utils/endpoint_alert_check'; import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__'; import { allCasesPermissions } from '../../../cases_test_utils'; import { HostStatus } from '../../../../common/endpoint/types'; import { ENDPOINT_CAPABILITIES } from '../../../../common/endpoint/service/response_actions/constants'; import { ALERT_TAGS_CONTEXT_MENU_ITEM_TITLE } from '../../../common/components/toolbar/bulk_actions/translations'; +import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config'; +jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../common/components/user_privileges'); jest.mock('../user_info', () => ({ @@ -92,6 +94,11 @@ jest.mock('../../containers/detection_engine/alerts/use_host_isolation_status', jest.mock('../../../common/components/user_privileges'); +const mockUseHasSecurityCapability = jest.fn().mockReturnValue(false); +jest.mock('../../../helper_hooks', () => ({ + useHasSecurityCapability: () => mockUseHasSecurityCapability(), +})); + describe('take action dropdown', () => { let defaultProps: TakeActionDropdownProps; let mockStartServicesMock: ReturnType; @@ -131,6 +138,12 @@ describe('take action dropdown', () => { }); (useHttp as jest.Mock).mockReturnValue(mockStartServicesMock.http); + + (useListsConfig as jest.Mock).mockImplementation(() => ({ + loading: false, + needsConfiguration: false, + })); + mockUseHasSecurityCapability.mockReturnValue(true); }); afterEach(() => { @@ -206,6 +219,7 @@ describe('take action dropdown', () => { ).toEqual('Add Endpoint exception'); }); }); + test('should render "Add rule exception"', async () => { await waitFor(() => { expect(wrapper.find('[data-test-subj="add-exception-menu-item"]').first().text()).toEqual( diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index e7703c86f23bb..67175f05ece2e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -10,6 +10,7 @@ import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { TableId } from '@kbn/securitysolution-data-table'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; import { AlertsCasesTourSteps, @@ -17,9 +18,8 @@ import { } from '../../../common/components/guided_onboarding_tour/tour_config'; import { isActiveTimeline } from '../../../helpers'; import { useResponderActionItem } from '../endpoint_responder'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TAKE_ACTION } from '../alerts_table/additional_filters_action/translations'; -import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; +import { useAlertExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline'; @@ -157,7 +157,7 @@ export const TakeActionDropdown = React.memo( [onAddExceptionTypeClick] ); - const { exceptionActionItems } = useExceptionActions({ + const { exceptionActionItems } = useAlertExceptionActions({ isEndpointAlert: isAlertFromEndpointAlert({ ecsData }), onAddExceptionTypeClick: handleOnAddExceptionTypeClick, });