Skip to content

Commit

Permalink
[SecuritySolution][Case] Disable cases on detections in read-only mode (
Browse files Browse the repository at this point in the history
#93010)

* Disable cases on detetions on read-only mode

* Add cypress tests
  • Loading branch information
cnasikas authored Mar 1, 2021
1 parent 4739eab commit aa62a13
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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"]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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',
});
Expand Down Expand Up @@ -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(
<TestProviders>
<AddToCaseAction
{...props}
ecsRowData={{
_id: 'test-id',
_index: 'test-index',
signal: { rule: { id: ['rule-id'], false_positives: [] } },
}}
/>
</TestProviders>
);

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',
Expand Down Expand Up @@ -291,4 +326,39 @@ describe('AddToCaseAction', () => {
path: '/selected-case',
});
});

it('disabled when event type is not supported', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction
{...props}
ecsRowData={{
_id: 'test-id',
_index: 'test-index',
}}
/>
</TestProviders>
);

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(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);

expect(
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('disabled')
).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { isEmpty } from 'lodash';
import React, { memo, useState, useCallback, useMemo } from 'react';
import {
EuiPopover,
Expand All @@ -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';
Expand All @@ -34,13 +35,11 @@ import { CreateCaseFlyout } from '../create/flyout';
interface AddToCaseActionProps {
ariaLabel?: string;
ecsRowData: Ecs;
disabled: boolean;
}

const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL,
ecsRowData,
disabled,
}) => {
const eventId = ecsRowData._id;
const eventIndex = ecsRowData._index;
Expand All @@ -51,6 +50,16 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
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();

Expand Down Expand Up @@ -137,7 +146,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
onClick={addNewCaseClick}
aria-label={i18n.ACTION_ADD_NEW_CASE}
data-test-subj="add-new-case-item"
disabled={disabled}
disabled={isDisabled}
>
<EuiText size="m">{i18n.ACTION_ADD_NEW_CASE}</EuiText>
</EuiContextMenuItem>,
Expand All @@ -146,31 +155,28 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
onClick={addExistingCaseClick}
aria-label={i18n.ACTION_ADD_EXISTING_CASE}
data-test-subj="add-existing-case-menu-item"
disabled={disabled}
disabled={isDisabled}
>
<EuiText size="m">{i18n.ACTION_ADD_EXISTING_CASE}</EuiText>
</EuiContextMenuItem>,
],
[addExistingCaseClick, addNewCaseClick, disabled]
[addExistingCaseClick, addNewCaseClick, isDisabled]
);

const button = useMemo(
() => (
<EuiToolTip
data-test-subj="attach-alert-to-case-tooltip"
content={i18n.ACTION_ADD_TO_CASE_TOOLTIP}
>
<EuiToolTip data-test-subj="attach-alert-to-case-tooltip" content={tooltipContext}>
<EuiButtonIcon
aria-label={ariaLabel}
data-test-subj="attach-alert-to-case-button"
size="s"
iconType="folderClosed"
onClick={openPopover}
disabled={disabled}
disabled={isDisabled}
/>
</EuiToolTip>
),
[ariaLabel, disabled, openPopover]
[ariaLabel, isDisabled, openPopover, tooltipContext]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ export const EventColumnView = React.memo<Props>(
ariaLabel={i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues })}
key="attach-to-case"
ecsRowData={ecsData}
disabled={eventType !== 'signal'}
/>,
]
: []),
Expand Down

0 comments on commit aa62a13

Please sign in to comment.