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'}
/>,
]
: []),