Skip to content

Commit

Permalink
[Security Solution] [Exceptions] Adds options to create a shared exce…
Browse files Browse the repository at this point in the history
…ption list and to create a single item from the manage exceptions view (#144575)

Adds options to create a shared exception list and creating a single
item to be attached to multiple rules default lists or to add it to
shared lists.

Co-authored-by: Gloria Hornero <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
3 people authored Nov 9, 2022
1 parent 9298023 commit b1179e7
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import React from 'react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { FieldProps } from './types';
import { useField } from './use_field';
Expand Down Expand Up @@ -62,7 +63,10 @@ export const FieldComponent: React.FC<FieldProps> = ({
data-test-subj="fieldAutocompleteComboBox"
style={fieldWidth}
onCreateOption={handleCreateCustomOption}
customOptionText="Add {searchValue} as your occupation"
customOptionText={i18n.translate('autocomplete.customOptionText', {
defaultMessage: 'Add {searchValuePlaceholder} as a custom field',
values: { searchValuePlaceholder: '{searchValue}' },
})}
fullWidth
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefi
export type ExceptionListType = t.TypeOf<typeof exceptionListType>;
export type ExceptionListTypeOrUndefined = t.TypeOf<typeof exceptionListTypeOrUndefined>;
export enum ExceptionListTypeEnum {
DETECTION = 'detection',
RULE_DEFAULT = 'rule_default',
DETECTION = 'detection', // shared exception list type
RULE_DEFAULT = 'rule_default', // rule default, cannot be shared
ENDPOINT = 'endpoint',
ENDPOINT_TRUSTED_APPS = 'endpoint',
ENDPOINT_EVENTS = 'endpoint_events',
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ export const LEGACY_NOTIFICATIONS_ID = `siem.notifications` as const;
*/
export const UPDATE_OR_CREATE_LEGACY_ACTIONS = '/internal/api/detection/legacy/notifications';

/**
* Exceptions management routes
*/

export const SHARED_EXCEPTION_LIST_URL = `/api${EXCEPTIONS_PATH}/shared` as const;

/**
* Detection engine routes
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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 type { ChangeEvent } from 'react';
import React, { memo, useState, useCallback, useRef, useEffect } from 'react';
import {
EuiFlyout,
EuiTitle,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiText,
EuiFieldText,
EuiSpacer,
EuiTextArea,
EuiFlyoutFooter,
EuiFlexGroup,
EuiButtonEmpty,
EuiButton,
EuiFlexItem,
} from '@elastic/eui';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { ErrorToastOptions, Toast, ToastInput } from '@kbn/core-notifications-browser';
import { i18n as translate } from '@kbn/i18n';
import type { ListDetails } from '@kbn/securitysolution-exception-list-components';

import { useCreateSharedExceptionListWithOptionalSignal } from './use_create_shared_list';
import {
CREATE_SHARED_LIST_TITLE,
CREATE_SHARED_LIST_NAME_FIELD,
CREATE_SHARED_LIST_DESCRIPTION,
CREATE_BUTTON,
CLOSE_FLYOUT,
CREATE_SHARED_LIST_DESCRIPTION_PLACEHOLDER,
CREATE_SHARED_LIST_NAME_FIELD_PLACEHOLDER,
SUCCESS_TITLE,
getSuccessText,
} from './translations';

export const CreateSharedListFlyout = memo(
({
handleRefresh,
http,
handleCloseFlyout,
addSuccess,
addError,
}: {
handleRefresh: () => void;
http: HttpSetup;
addSuccess: (toastOrTitle: ToastInput, options?: unknown) => Toast;
addError: (error: unknown, options: ErrorToastOptions) => Toast;
handleCloseFlyout: () => void;
}) => {
const { start: createSharedExceptionList, ...createSharedExceptionListState } =
useCreateSharedExceptionListWithOptionalSignal();
const ctrl = useRef(new AbortController());

enum DetailProperty {
name = 'name',
description = 'description',
}

const [newListDetails, setNewListDetails] = useState<ListDetails>({
name: '',
description: '',
});
const onChange = (
{ target }: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
detailProperty: DetailProperty.name | DetailProperty.description
) => {
const { value } = target;
setNewListDetails({ ...newListDetails, [detailProperty]: value });
};

const handleCreateSharedExceptionList = useCallback(() => {
if (!createSharedExceptionListState.loading && newListDetails.name !== '') {
ctrl.current = new AbortController();

createSharedExceptionList({
http,
signal: ctrl.current.signal,
name: newListDetails.name,
description: newListDetails.description ?? '',
});
}
}, [createSharedExceptionList, createSharedExceptionListState.loading, newListDetails, http]);

const handleCreateSuccess = useCallback(
(response) => {
addSuccess({
text: getSuccessText(newListDetails.name),
title: SUCCESS_TITLE,
});
handleRefresh();

handleCloseFlyout();
},
[addSuccess, handleCloseFlyout, handleRefresh, newListDetails]
);

const handleCreateError = useCallback(
(error) => {
if (!error.message.includes('AbortError') && !error?.body?.message.includes('AbortError')) {
addError(error, {
title: translate.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListErrorTitle',
{
defaultMessage: 'creation error',
}
),
});
}
},
[addError]
);

useEffect(() => {
if (!createSharedExceptionListState.loading) {
if (createSharedExceptionListState?.result) {
handleCreateSuccess(createSharedExceptionListState.result);
} else if (createSharedExceptionListState?.error) {
handleCreateError(createSharedExceptionListState?.error);
}
}
}, [
createSharedExceptionListState?.error,
createSharedExceptionListState.loading,
createSharedExceptionListState.result,
handleCreateError,
handleCreateSuccess,
]);

return (
<EuiFlyout
ownFocus
size="s"
onClose={handleCloseFlyout}
data-test-subj="createSharedExceptionListFlyout"
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 data-test-subj="createSharedExceptionListTitle">{CREATE_SHARED_LIST_TITLE}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>{CREATE_SHARED_LIST_NAME_FIELD}</EuiText>
<EuiFieldText
placeholder={CREATE_SHARED_LIST_NAME_FIELD_PLACEHOLDER}
value={newListDetails.name}
onChange={(e) => onChange(e, DetailProperty.name)}
aria-label="Use aria labels when no actual label is in use"
/>
<EuiSpacer />
<EuiText>{CREATE_SHARED_LIST_DESCRIPTION}</EuiText>
<EuiTextArea
placeholder={CREATE_SHARED_LIST_DESCRIPTION_PLACEHOLDER}
value={newListDetails.description}
onChange={(e) => onChange(e, DetailProperty.description)}
aria-label="Stop the hackers"
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={handleCloseFlyout} flush="left">
{CLOSE_FLYOUT}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="exception-lists-form-create-shared"
onClick={handleCreateSharedExceptionList}
disabled={newListDetails.name === ''}
>
{CREATE_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
);

CreateSharedListFlyout.displayName = 'CreateSharedListFlyout';
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../common/endpoint/service
import { ExceptionsListCard } from './exceptions_list_card';

import { ImportExceptionListFlyout } from './import_exceptions_list_flyout';
import { CreateSharedListFlyout } from './create_shared_exception_list';

import { AddExceptionFlyout } from '../../detection_engine/rule_exceptions/components/add_exception_flyout';

export type Func = () => Promise<void>;

Expand Down Expand Up @@ -355,6 +358,16 @@ export const ExceptionListsTable = React.memo(() => {

const goToPage = (pageNumber: number) => setActivePage(pageNumber);

const [isCreatePopoverOpen, setIsCreatePopoverOpen] = useState(false);
const [displayAddExceptionItemFlyout, setDisplayAddExceptionItemFlyout] = useState(false);
const [displayCreateSharedListFlyout, setDisplayCreateSharedListFlyout] = useState(false);

const onCreateButtonClick = () => setIsCreatePopoverOpen((isOpen) => !isOpen);
const onCloseCreatePopover = () => {
setDisplayAddExceptionItemFlyout(false);
setIsCreatePopoverOpen(false);
};

return (
<>
<MissingPrivilegesCallOut />
Expand All @@ -370,11 +383,67 @@ export const ExceptionListsTable = React.memo(() => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType={'importAction'} onClick={() => setDisplayImportListFlyout(true)}>
{i18n.IMPORT_EXCEPTION_LIST}
{i18n.IMPORT_EXCEPTION_LIST_BUTTON}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
data-test-subj="manageExceptionListCreateButton"
button={
<EuiButton iconType={'arrowDown'} onClick={onCreateButtonClick}>
{i18n.CREATE_BUTTON}
</EuiButton>
}
isOpen={isCreatePopoverOpen}
closePopover={onCloseCreatePopover}
>
<EuiContextMenuPanel
items={[
<EuiContextMenuItem
key={'createList'}
onClick={() => {
onCloseCreatePopover();
setDisplayCreateSharedListFlyout(true);
}}
>
{i18n.CREATE_SHARED_LIST_BUTTON}
</EuiContextMenuItem>,
<EuiContextMenuItem
key={'createItem'}
onClick={() => {
onCloseCreatePopover();
setDisplayAddExceptionItemFlyout(true);
}}
>
{i18n.CREATE_BUTTON_ITEM_BUTTON}
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>

{displayCreateSharedListFlyout && (
<CreateSharedListFlyout
handleRefresh={handleRefresh}
http={http}
addSuccess={addSuccess}
addError={addError}
handleCloseFlyout={() => setDisplayCreateSharedListFlyout(false)}
/>
)}

{displayAddExceptionItemFlyout && (
<AddExceptionFlyout
rules={null}
isEndpointItem={false}
isBulkAction={false}
showAlertCloseOptions
onCancel={(didRuleChange: boolean) => setDisplayAddExceptionItemFlyout(false)}
onConfirm={(didRuleChange: boolean) => setDisplayAddExceptionItemFlyout(false)}
/>
)}

{displayImportListFlyout && (
<ImportExceptionListFlyout
handleRefresh={handleRefresh}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const EXPORT_EXCEPTION_LIST = i18n.translate(
);

export const IMPORT_EXCEPTION_LIST_HEADER = i18n.translate(
'xpack.securitySolution.exceptionsTable.importExceptionListFlyoutHeader',
'xpack.securitySolution.exceptionsTable.importExceptionListHeader',
{
defaultMessage: 'Import shared exception list',
}
Expand Down Expand Up @@ -104,3 +104,58 @@ export const IMPORT_PROMPT = i18n.translate(
defaultMessage: 'Select or drag and drop multiple files',
}
);

export const CREATE_SHARED_LIST_TITLE = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListTitle',
{
defaultMessage: 'Create shared exception list',
}
);

export const CREATE_SHARED_LIST_NAME_FIELD = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutNameField',
{
defaultMessage: 'Shared exception list name',
}
);

export const CREATE_SHARED_LIST_NAME_FIELD_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutNameFieldPlaceholder',
{
defaultMessage: 'New exception list',
}
);

export const CREATE_SHARED_LIST_DESCRIPTION = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutDescription',
{
defaultMessage: 'Description (optional)',
}
);

export const CREATE_SHARED_LIST_DESCRIPTION_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutDescriptionPlaceholder',
{
defaultMessage: 'New exception list',
}
);

export const CREATE_BUTTON = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListFlyoutCreateButton',
{
defaultMessage: 'Create shared exception list',
}
);

export const getSuccessText = (listName: string) =>
i18n.translate('xpack.securitySolution.exceptions.createSharedExceptionListSuccessDescription', {
defaultMessage: 'list with name ${listName} was created!',
values: { listName },
});

export const SUCCESS_TITLE = i18n.translate(
'xpack.securitySolution.exceptions.createSharedExceptionListSuccessTitle',
{
defaultMessage: 'created list',
}
);
Loading

0 comments on commit b1179e7

Please sign in to comment.