Skip to content

Commit 19613d4

Browse files
cnasikaskibanamachine
authored andcommitted
[SecuritySolution][Case] Disable cases on detections in read-only mode (#93010)
* Disable cases on detetions on read-only mode * Add cypress tests
1 parent 168ef90 commit 19613d4

6 files changed

Lines changed: 170 additions & 18 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { newRule } from '../../objects/rule';
9+
import { ROLES } from '../../../common/test';
10+
11+
import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
12+
import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
13+
import { cleanKibana } from '../../tasks/common';
14+
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
15+
import { login, loginAndWaitForPage, waitForPageWithoutDateRange } from '../../tasks/login';
16+
import { refreshPage } from '../../tasks/security_header';
17+
18+
import { DETECTIONS_URL } from '../../urls/navigation';
19+
import { ATTACH_ALERT_TO_CASE_BUTTON } from '../../screens/alerts_detection_rules';
20+
21+
const loadDetectionsPage = (role: ROLES) => {
22+
waitForPageWithoutDateRange(DETECTIONS_URL, role);
23+
waitForAlertsToPopulate();
24+
};
25+
26+
describe('Alerts timeline', () => {
27+
before(() => {
28+
// First we login as a privileged user to create alerts.
29+
cleanKibana();
30+
loginAndWaitForPage(DETECTIONS_URL, ROLES.platform_engineer);
31+
waitForAlertsPanelToBeLoaded();
32+
waitForAlertsIndexToBeCreated();
33+
createCustomRuleActivated(newRule);
34+
refreshPage();
35+
waitForAlertsToPopulate();
36+
37+
// Then we login as read-only user to test.
38+
login(ROLES.reader);
39+
});
40+
41+
context('Privileges: read only', () => {
42+
beforeEach(() => {
43+
loadDetectionsPage(ROLES.reader);
44+
});
45+
46+
it('should not allow user with read only privileges to attach alerts to cases', () => {
47+
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('be.disabled');
48+
});
49+
});
50+
51+
context('Privileges: can crud', () => {
52+
beforeEach(() => {
53+
loadDetectionsPage(ROLES.platform_engineer);
54+
});
55+
56+
it('should allow a user with crud privileges to attach alerts to cases', () => {
57+
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('not.be.disabled');
58+
});
59+
});
60+
});

x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* 2.0.
66
*/
77

8+
export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]';
9+
810
export const BULK_ACTIONS_BTN = '[data-test-subj="bulkActions"] span';
911

1012
export const CREATE_NEW_RULE_BTN = '[data-test-subj="create-new-rule"]';

x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import React, { ReactNode } from 'react';
1010
import { mount } from 'enzyme';
1111
import { EuiGlobalToastList } from '@elastic/eui';
1212

13-
import { useKibana } from '../../../common/lib/kibana';
13+
import { useKibana, useGetUserSavedObjectPermissions } from '../../../common/lib/kibana';
1414
import { useStateToaster } from '../../../common/components/toasters';
1515
import { TestProviders } from '../../../common/mock';
1616
import { usePostComment } from '../../containers/use_post_comment';
@@ -113,8 +113,8 @@ describe('AddToCaseAction', () => {
113113
ecsRowData: {
114114
_id: 'test-id',
115115
_index: 'test-index',
116+
signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } },
116117
},
117-
disabled: false,
118118
};
119119

120120
const mockDispatchToaster = jest.fn();
@@ -127,6 +127,10 @@ describe('AddToCaseAction', () => {
127127
(useKibana as jest.Mock).mockReturnValue({
128128
services: { application: { navigateToApp: mockNavigateToApp } },
129129
});
130+
(useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({
131+
crud: true,
132+
read: true,
133+
});
130134
});
131135

132136
it('it renders', async () => {
@@ -181,8 +185,8 @@ describe('AddToCaseAction', () => {
181185
alertId: 'test-id',
182186
index: 'test-index',
183187
rule: {
184-
id: null,
185-
name: null,
188+
id: 'rule-id',
189+
name: 'rule-name',
186190
},
187191
type: 'alert',
188192
});
@@ -218,7 +222,38 @@ describe('AddToCaseAction', () => {
218222
alertId: 'test-id',
219223
index: 'test-index',
220224
rule: {
221-
id: null,
225+
id: 'rule-id',
226+
name: 'rule-name',
227+
},
228+
type: 'alert',
229+
});
230+
});
231+
232+
it('it set rule information as null when missing', async () => {
233+
const wrapper = mount(
234+
<TestProviders>
235+
<AddToCaseAction
236+
{...props}
237+
ecsRowData={{
238+
_id: 'test-id',
239+
_index: 'test-index',
240+
signal: { rule: { id: ['rule-id'], false_positives: [] } },
241+
}}
242+
/>
243+
</TestProviders>
244+
);
245+
246+
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
247+
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
248+
249+
wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click');
250+
251+
expect(postComment.mock.calls[0][0].caseId).toBe('new-case');
252+
expect(postComment.mock.calls[0][0].data).toEqual({
253+
alertId: 'test-id',
254+
index: 'test-index',
255+
rule: {
256+
id: 'rule-id',
222257
name: null,
223258
},
224259
type: 'alert',
@@ -291,4 +326,39 @@ describe('AddToCaseAction', () => {
291326
path: '/selected-case',
292327
});
293328
});
329+
330+
it('disabled when event type is not supported', async () => {
331+
const wrapper = mount(
332+
<TestProviders>
333+
<AddToCaseAction
334+
{...props}
335+
ecsRowData={{
336+
_id: 'test-id',
337+
_index: 'test-index',
338+
}}
339+
/>
340+
</TestProviders>
341+
);
342+
343+
expect(
344+
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled')
345+
).toBeTruthy();
346+
});
347+
348+
it('disabled when user does not have crud permissions', async () => {
349+
(useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({
350+
crud: false,
351+
read: true,
352+
});
353+
354+
const wrapper = mount(
355+
<TestProviders>
356+
<AddToCaseAction {...props} />
357+
</TestProviders>
358+
);
359+
360+
expect(
361+
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled')
362+
).toBeTruthy();
363+
});
294364
});

x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* 2.0.
66
*/
77

8+
import { isEmpty } from 'lodash';
89
import React, { memo, useState, useCallback, useMemo } from 'react';
910
import {
1011
EuiPopover,
@@ -22,7 +23,7 @@ import { usePostComment } from '../../containers/use_post_comment';
2223
import { Case } from '../../containers/types';
2324
import { useStateToaster } from '../../../common/components/toasters';
2425
import { APP_ID } from '../../../../common/constants';
25-
import { useKibana } from '../../../common/lib/kibana';
26+
import { useGetUserSavedObjectPermissions, useKibana } from '../../../common/lib/kibana';
2627
import { getCaseDetailsUrl } from '../../../common/components/link_to';
2728
import { SecurityPageName } from '../../../app/types';
2829
import { useAllCasesModal } from '../use_all_cases_modal';
@@ -34,13 +35,11 @@ import { CreateCaseFlyout } from '../create/flyout';
3435
interface AddToCaseActionProps {
3536
ariaLabel?: string;
3637
ecsRowData: Ecs;
37-
disabled: boolean;
3838
}
3939

4040
const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
4141
ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL,
4242
ecsRowData,
43-
disabled,
4443
}) => {
4544
const eventId = ecsRowData._id;
4645
const eventIndex = ecsRowData._index;
@@ -51,6 +50,16 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
5150
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
5251
const openPopover = useCallback(() => setIsPopoverOpen(true), []);
5352
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
53+
const userPermissions = useGetUserSavedObjectPermissions();
54+
55+
const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id);
56+
const userCanCrud = userPermissions?.crud ?? false;
57+
const isDisabled = !userCanCrud || !isEventSupported;
58+
const tooltipContext = userCanCrud
59+
? isEventSupported
60+
? i18n.ACTION_ADD_TO_CASE_TOOLTIP
61+
: i18n.UNSUPPORTED_EVENTS_MSG
62+
: i18n.PERMISSIONS_MSG;
5463

5564
const { postComment } = usePostComment();
5665

@@ -137,7 +146,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
137146
onClick={addNewCaseClick}
138147
aria-label={i18n.ACTION_ADD_NEW_CASE}
139148
data-test-subj="add-new-case-item"
140-
disabled={disabled}
149+
disabled={isDisabled}
141150
>
142151
<EuiText size="m">{i18n.ACTION_ADD_NEW_CASE}</EuiText>
143152
</EuiContextMenuItem>,
@@ -146,31 +155,28 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
146155
onClick={addExistingCaseClick}
147156
aria-label={i18n.ACTION_ADD_EXISTING_CASE}
148157
data-test-subj="add-existing-case-menu-item"
149-
disabled={disabled}
158+
disabled={isDisabled}
150159
>
151160
<EuiText size="m">{i18n.ACTION_ADD_EXISTING_CASE}</EuiText>
152161
</EuiContextMenuItem>,
153162
],
154-
[addExistingCaseClick, addNewCaseClick, disabled]
163+
[addExistingCaseClick, addNewCaseClick, isDisabled]
155164
);
156165

157166
const button = useMemo(
158167
() => (
159-
<EuiToolTip
160-
data-test-subj="attach-alert-to-case-tooltip"
161-
content={i18n.ACTION_ADD_TO_CASE_TOOLTIP}
162-
>
168+
<EuiToolTip data-test-subj="attach-alert-to-case-tooltip" content={tooltipContext}>
163169
<EuiButtonIcon
164170
aria-label={ariaLabel}
165171
data-test-subj="attach-alert-to-case-button"
166172
size="s"
167173
iconType="folderClosed"
168174
onClick={openPopover}
169-
disabled={disabled}
175+
disabled={isDisabled}
170176
/>
171177
</EuiToolTip>
172178
),
173-
[ariaLabel, disabled, openPopover]
179+
[ariaLabel, isDisabled, openPopover, tooltipContext]
174180
);
175181

176182
return (

x-pack/plugins/security_solution/public/cases/components/timeline_actions/translations.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,18 @@ export const VIEW_CASE = i18n.translate(
6161
defaultMessage: 'View Case',
6262
}
6363
);
64+
65+
export const PERMISSIONS_MSG = i18n.translate(
66+
'xpack.securitySolution.case.timeline.actions.permissionsMessage',
67+
{
68+
defaultMessage:
69+
'You are currently missing the required permissions to attach alerts to cases. Please contact your administrator for further assistance.',
70+
}
71+
);
72+
73+
export const UNSUPPORTED_EVENTS_MSG = i18n.translate(
74+
'xpack.securitySolution.case.timeline.actions.unsupportedEventsMessage',
75+
{
76+
defaultMessage: 'This event cannot be attached to a case',
77+
}
78+
);

x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ export const EventColumnView = React.memo<Props>(
175175
ariaLabel={i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues })}
176176
key="attach-to-case"
177177
ecsRowData={ecsData}
178-
disabled={eventType !== 'signal'}
179178
/>,
180179
]
181180
: []),

0 commit comments

Comments
 (0)