[Security Solution] Display warning banner when removing a host isolation exception (#121729)

This commit is contained in:
Esteban Beltran 2021-12-21 22:05:15 +01:00 committed by GitHub
parent ea24d162d6
commit 9ae1ffe092
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 114 additions and 58 deletions

View file

@ -0,0 +1,24 @@
/*
* 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 { EuiButton, EuiButtonProps, PropsForButton } from '@elastic/eui';
import React, { FC, memo, useEffect, useRef } from 'react';
export const AutoFocusButton: FC<PropsForButton<EuiButtonProps>> = memo((props) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const button = <EuiButton buttonRef={buttonRef} {...props} />;
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.focus();
}
}, []);
return button;
});
AutoFocusButton.displayName = 'AutoFocusButton';

View file

@ -73,3 +73,12 @@ export function getEffectedPolicySelectionByTags(
export function isGlobalPolicyEffected(tags?: string[]): boolean {
return tags !== undefined && tags.find((tag) => tag === GLOBAL_POLICY_TAG) !== undefined;
}
/**
* Given an array of an artifact tags, return the ids of policies inside
* those tags. It will only return tags starting with `policy:` and it will
* return them without the suffix
*/
export function getArtifactPoliciesIdByTag(tags: string[] = []): string[] {
return tags.filter((tag) => tag.startsWith('policy:')).map((tag) => tag.substring(7));
}

View file

@ -5,9 +5,7 @@
* 2.0.
*/
import React, { memo, useCallback, useEffect } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiModal,
@ -18,20 +16,25 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { memo, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { i18n } from '@kbn/i18n';
import { useEventFiltersSelector } from '../hooks';
import { AutoFocusButton } from '../../../../../common/components/autofocus_button/autofocus_button';
import { useToasts } from '../../../../../common/lib/kibana';
import { AppAction } from '../../../../../common/store/actions';
import {
getArtifactPoliciesIdByTag,
isGlobalPolicyEffected,
} from '../../../../components/effected_policy_select/utils';
import {
getDeleteError,
getItemToDelete,
isDeletionInProgress,
wasDeletionSuccessful,
} from '../../store/selector';
import { AppAction } from '../../../../../common/store/actions';
import { useToasts } from '../../../../../common/lib/kibana';
import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils';
import { useEventFiltersSelector } from '../hooks';
export const EventFilterDeleteModal = memo<{}>(() => {
const dispatch = useDispatch<Dispatch<AppAction>>();
@ -109,7 +112,7 @@ export const EventFilterDeleteModal = memo<{}>(() => {
values={{
count: isGlobalPolicyEffected(Array.from(eventFilter?.tags || []))
? 'all'
: eventFilter?.tags?.length || 0,
: getArtifactPoliciesIdByTag(eventFilter?.tags as string[]).length,
}}
/>
</p>
@ -136,7 +139,7 @@ export const EventFilterDeleteModal = memo<{}>(() => {
/>
</EuiButtonEmpty>
<EuiButton
<AutoFocusButton
fill
color="danger"
onClick={onConfirm}
@ -147,7 +150,7 @@ export const EventFilterDeleteModal = memo<{}>(() => {
id="xpack.securitySolution.eventFilters.deletionDialog.confirmButton"
defaultMessage="Delete"
/>
</EuiButton>
</AutoFocusButton>
</EuiModalFooter>
</EuiModal>
);

View file

@ -4,16 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import React from 'react';
import uuid from 'uuid';
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import {
AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../common/mock/endpoint';
import { HostIsolationExceptionDeleteModal } from './delete_modal';
import { deleteOneHostIsolationExceptionItem } from '../../service';
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import { HostIsolationExceptionDeleteModal } from './delete_modal';
jest.mock('../../service');
const deleteOneHostIsolationExceptionItemMock = deleteOneHostIsolationExceptionItem as jest.Mock;
@ -23,10 +25,11 @@ describe('When on the host isolation exceptions delete modal', () => {
let renderResult: ReturnType<typeof render>;
let coreStart: AppContextTestRender['coreStart'];
let onCancel: (forceRefresh?: boolean) => void;
let itemToDelete: ExceptionListItemSchema;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
const itemToDelete = getExceptionListItemSchemaMock();
itemToDelete = getExceptionListItemSchemaMock();
deleteOneHostIsolationExceptionItemMock.mockReset();
onCancel = jest.fn();
render = () =>
@ -44,6 +47,24 @@ describe('When on the host isolation exceptions delete modal', () => {
).toBeTruthy();
});
it.each(['all', '0', '1', '2', '5'])(
'should display a warning banner with how many policies (%s) will be affected. skipping non-policy-tags',
(amount) => {
if (amount === 'all') {
itemToDelete.tags = ['policy:all', 'non-policy-tag'];
} else {
itemToDelete.tags = [
...Array.from({ length: +amount }, (_) => `policy:${uuid.v4()}`),
'non-policy-tag',
];
}
render();
expect(
renderResult.getByTestId('hostIsolationExceptionsDeleteModalCalloutMessage')
).toHaveTextContent(`Deleting this entry will remove it from ${amount} associated`);
}
);
it('should disable the buttons when confirm is pressed and show loading', async () => {
render();

View file

@ -5,10 +5,9 @@
* 2.0.
*/
import React, { memo } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiModal,
EuiModalBody,
EuiModalFooter,
@ -16,11 +15,17 @@ import {
EuiModalHeaderTitle,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import React, { memo } from 'react';
import { useMutation } from 'react-query';
import { AutoFocusButton } from '../../../../../common/components/autofocus_button/autofocus_button';
import { useHttp, useToasts } from '../../../../../common/lib/kibana';
import {
getArtifactPoliciesIdByTag,
isGlobalPolicyEffected,
} from '../../../../components/effected_policy_select/utils';
import { deleteOneHostIsolationExceptionItem } from '../../service';
export const HostIsolationExceptionDeleteModal = memo(
@ -89,13 +94,29 @@ export const HostIsolationExceptionDeleteModal = memo(
<EuiModalBody data-test-subj="hostIsolationExceptionsFilterDeleteModalBody">
<EuiText>
<p>
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.subtitle"
defaultMessage='You are deleting exception "{name}".'
values={{ name: <b className="eui-textBreakWord">{item?.name}</b> }}
/>
</p>
<EuiCallOut
data-test-subj="hostIsolationExceptionsDeleteModalCallout"
title={i18n.translate(
'xpack.securitySolution.hostIsolationExceptions.deletionDialog.calloutTitle',
{
defaultMessage: 'Warning',
}
)}
color="danger"
iconType="alert"
>
<p data-test-subj="hostIsolationExceptionsDeleteModalCalloutMessage">
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.calloutMessage"
defaultMessage="Deleting this entry will remove it from {count} associated {count, plural, one {policy} other {policies}}."
values={{
count: isGlobalPolicyEffected(Array.from(item.tags || []))
? 'all'
: getArtifactPoliciesIdByTag(item.tags).length,
}}
/>
</p>
</EuiCallOut>
<p>
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmation"
@ -117,7 +138,7 @@ export const HostIsolationExceptionDeleteModal = memo(
/>
</EuiButtonEmpty>
<EuiButton
<AutoFocusButton
fill
color="danger"
onClick={handleConfirmButton}
@ -126,9 +147,9 @@ export const HostIsolationExceptionDeleteModal = memo(
>
<FormattedMessage
id="xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmButton"
defaultMessage="Remove exception"
defaultMessage="Delete"
/>
</EuiButton>
</AutoFocusButton>
</EuiModalFooter>
</EuiModal>
);

View file

@ -5,34 +5,31 @@
* 2.0.
*/
import React, { FC, memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiButtonEmpty,
EuiButtonProps,
PropsForButton,
EuiCallOut,
EuiSpacer,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { memo, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { AutoFocusButton } from '../../../../common/components/autofocus_button/autofocus_button';
import { Immutable, TrustedApp } from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import { useTrustedAppsSelector } from './hooks';
import { isPolicyEffectScope } from '../state/type_guards';
import {
getDeletionDialogEntry,
isDeletionDialogOpen,
isDeletionInProgress,
} from '../store/selectors';
import { isPolicyEffectScope } from '../state/type_guards';
import { useTrustedAppsSelector } from './hooks';
const CANCEL_SUBJ = 'trustedAppDeletionCancel';
const CONFIRM_SUBJ = 'trustedAppDeletionConfirm';
@ -83,21 +80,6 @@ const getTranslations = (entry: Immutable<TrustedApp> | undefined) => ({
),
});
const AutoFocusButton: FC<PropsForButton<EuiButtonProps>> = memo((props) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const button = <EuiButton buttonRef={buttonRef} {...props} />;
useEffect(() => {
if (buttonRef.current) {
buttonRef.current.focus();
}
}, []);
return button;
});
AutoFocusButton.displayName = 'AutoFocusButton';
export const TrustedAppDeletionDialog = memo(() => {
const dispatch = useDispatch<Dispatch<AppAction>>();
const isBusy = useTrustedAppsSelector(isDeletionInProgress);

View file

@ -23007,10 +23007,8 @@
"xpack.securitySolution.hostIsolation.agentStatuses.empty": "-",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.cancel": "キャンセル",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmation": "この操作は元に戻すことができません。続行していいですか?",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmButton": "例外を削除",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteFailure": "ホスト分離例外リストから\"{name}\"を削除できません。理由:{message}",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteSuccess": "\"{name}\"はホスト分離例外リストから削除されました。",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.subtitle": "例外\"{name}\"を削除しています。",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.title": "ホスト分離例外を削除",
"xpack.securitySolution.hostIsolationExceptions.flyout.cancel": "キャンセル",
"xpack.securitySolution.hostIsolationExceptions.flyout.createButton": "ホスト分離例外を追加",

View file

@ -23374,10 +23374,8 @@
"xpack.securitySolution.hostIsolation.agentStatuses.empty": "-",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.cancel": "取消",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmation": "此操作无法撤消。是否确定要继续?",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.confirmButton": "移除例外",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteFailure": "无法从主机隔离例外列表中移除“{name}”。原因:{message}",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.deleteSuccess": "已从主机隔离例外列表中移除“{name}”。",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.subtitle": "正在删除例外“{name}”。",
"xpack.securitySolution.hostIsolationExceptions.deletionDialog.title": "删除主机隔离例外",
"xpack.securitySolution.hostIsolationExceptions.flyout.cancel": "取消",
"xpack.securitySolution.hostIsolationExceptions.flyout.createButton": "添加主机隔离例外",