diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts new file mode 100644 index 00000000000000..e63ef513cc6382 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts @@ -0,0 +1,60 @@ +/* + * 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 { newRule } from '../../objects/rule'; +import { ROLES } from '../../../common/test'; + +import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts'; +import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login, loginAndWaitForPage, waitForPageWithoutDateRange } from '../../tasks/login'; +import { refreshPage } from '../../tasks/security_header'; + +import { DETECTIONS_URL } from '../../urls/navigation'; +import { ATTACH_ALERT_TO_CASE_BUTTON } from '../../screens/alerts_detection_rules'; + +const loadDetectionsPage = (role: ROLES) => { + waitForPageWithoutDateRange(DETECTIONS_URL, role); + waitForAlertsToPopulate(); +}; + +describe('Alerts timeline', () => { + before(() => { + // First we login as a privileged user to create alerts. + cleanKibana(); + loginAndWaitForPage(DETECTIONS_URL, ROLES.platform_engineer); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + createCustomRuleActivated(newRule); + refreshPage(); + waitForAlertsToPopulate(); + + // Then we login as read-only user to test. + login(ROLES.reader); + }); + + context('Privileges: read only', () => { + beforeEach(() => { + loadDetectionsPage(ROLES.reader); + }); + + it('should not allow user with read only privileges to attach alerts to cases', () => { + cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('be.disabled'); + }); + }); + + context('Privileges: can crud', () => { + beforeEach(() => { + loadDetectionsPage(ROLES.platform_engineer); + }); + + it('should allow a user with crud privileges to attach alerts to cases', () => { + cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('not.be.disabled'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 30365c9bd4c708..c74284eee15e41 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]'; + export const BULK_ACTIONS_BTN = '[data-test-subj="bulkActions"] span'; export const CREATE_NEW_RULE_BTN = '[data-test-subj="create-new-rule"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index b3302a05cfcb21..c99cabb50e3dc2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -10,7 +10,7 @@ import React, { ReactNode } from 'react'; import { mount } from 'enzyme'; import { EuiGlobalToastList } from '@elastic/eui'; -import { useKibana } from '../../../common/lib/kibana'; +import { useKibana, useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; import { useStateToaster } from '../../../common/components/toasters'; import { TestProviders } from '../../../common/mock'; import { usePostComment } from '../../containers/use_post_comment'; @@ -113,8 +113,8 @@ describe('AddToCaseAction', () => { ecsRowData: { _id: 'test-id', _index: 'test-index', + signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } }, }, - disabled: false, }; const mockDispatchToaster = jest.fn(); @@ -127,6 +127,10 @@ describe('AddToCaseAction', () => { (useKibana as jest.Mock).mockReturnValue({ services: { application: { navigateToApp: mockNavigateToApp } }, }); + (useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({ + crud: true, + read: true, + }); }); it('it renders', async () => { @@ -181,8 +185,8 @@ describe('AddToCaseAction', () => { alertId: 'test-id', index: 'test-index', rule: { - id: null, - name: null, + id: 'rule-id', + name: 'rule-name', }, type: 'alert', }); @@ -218,7 +222,38 @@ describe('AddToCaseAction', () => { alertId: 'test-id', index: 'test-index', rule: { - id: null, + id: 'rule-id', + name: 'rule-name', + }, + type: 'alert', + }); + }); + + it('it set rule information as null when missing', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); + + wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + + expect(postComment.mock.calls[0][0].caseId).toBe('new-case'); + expect(postComment.mock.calls[0][0].data).toEqual({ + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'rule-id', name: null, }, type: 'alert', @@ -291,4 +326,39 @@ describe('AddToCaseAction', () => { path: '/selected-case', }); }); + + it('disabled when event type is not supported', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled') + ).toBeTruthy(); + }); + + it('disabled when user does not have crud permissions', async () => { + (useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: true, + }); + + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled') + ).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 3000551dd3c07f..4a4420a164d0cb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import React, { memo, useState, useCallback, useMemo } from 'react'; import { EuiPopover, @@ -22,7 +23,7 @@ import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { useStateToaster } from '../../../common/components/toasters'; import { APP_ID } from '../../../../common/constants'; -import { useKibana } from '../../../common/lib/kibana'; +import { useGetUserSavedObjectPermissions, useKibana } from '../../../common/lib/kibana'; import { getCaseDetailsUrl } from '../../../common/components/link_to'; import { SecurityPageName } from '../../../app/types'; import { useAllCasesModal } from '../use_all_cases_modal'; @@ -34,13 +35,11 @@ import { CreateCaseFlyout } from '../create/flyout'; interface AddToCaseActionProps { ariaLabel?: string; ecsRowData: Ecs; - disabled: boolean; } const AddToCaseActionComponent: React.FC = ({ ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL, ecsRowData, - disabled, }) => { const eventId = ecsRowData._id; const eventIndex = ecsRowData._index; @@ -51,6 +50,16 @@ const AddToCaseActionComponent: React.FC = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const openPopover = useCallback(() => setIsPopoverOpen(true), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const userPermissions = useGetUserSavedObjectPermissions(); + + const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id); + const userCanCrud = userPermissions?.crud ?? false; + const isDisabled = !userCanCrud || !isEventSupported; + const tooltipContext = userCanCrud + ? isEventSupported + ? i18n.ACTION_ADD_TO_CASE_TOOLTIP + : i18n.UNSUPPORTED_EVENTS_MSG + : i18n.PERMISSIONS_MSG; const { postComment } = usePostComment(); @@ -137,7 +146,7 @@ const AddToCaseActionComponent: React.FC = ({ onClick={addNewCaseClick} aria-label={i18n.ACTION_ADD_NEW_CASE} data-test-subj="add-new-case-item" - disabled={disabled} + disabled={isDisabled} > {i18n.ACTION_ADD_NEW_CASE} , @@ -146,31 +155,28 @@ const AddToCaseActionComponent: React.FC = ({ onClick={addExistingCaseClick} aria-label={i18n.ACTION_ADD_EXISTING_CASE} data-test-subj="add-existing-case-menu-item" - disabled={disabled} + disabled={isDisabled} > {i18n.ACTION_ADD_EXISTING_CASE} , ], - [addExistingCaseClick, addNewCaseClick, disabled] + [addExistingCaseClick, addNewCaseClick, isDisabled] ); const button = useMemo( () => ( - + ), - [ariaLabel, disabled, openPopover] + [ariaLabel, isDisabled, openPopover, tooltipContext] ); return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts index da07b1b79cee9d..7c1437ede9ce50 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts @@ -61,3 +61,18 @@ export const VIEW_CASE = i18n.translate( defaultMessage: 'View Case', } ); + +export const PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.case.timeline.actions.permissionsMessage', + { + defaultMessage: + 'You are currently missing the required permissions to attach alerts to cases. Please contact your administrator for further assistance.', + } +); + +export const UNSUPPORTED_EVENTS_MSG = i18n.translate( + 'xpack.securitySolution.case.timeline.actions.unsupportedEventsMessage', + { + defaultMessage: 'This event cannot be attached to a case', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 9d7b76af25a59d..c6caf0a7b5b155 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -175,7 +175,6 @@ export const EventColumnView = React.memo( ariaLabel={i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues })} key="attach-to-case" ecsRowData={ecsData} - disabled={eventType !== 'signal'} />, ] : []),