Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solutions][Detections] - Fix exception list table referential deletion #87231

Merged
merged 6 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/lists/public/exceptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export interface ApiCallByIdProps {
export interface ApiCallMemoProps {
id: string;
namespaceType: NamespaceType;
onError: (arg: string[]) => void;
onError: (arg: Error) => void;
onSuccess: () => void;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"list_id": "detection_list_1",
"item_id": "simple_list_item_two_non-value_list",
"item_id": "simple_list_item_one_non-value_list",
yctercero marked this conversation as resolved.
Show resolved Hide resolved
"tags": [
"user added string for a tag",
"malware"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"list_id": "detection_list_2",
"item_id": "simple_list_item_two_non-value_list",
"tags": [
"user added string for a tag",
"malware"
],
"type": "simple",
"description": "This is a sample exception list item with two non-value list entries",
"name": "Sample Detection Exception List Item",
"os_types": [
"windows"
],
"comments": [],
"entries": [
{
"field": "actingProcess.file.signer",
"operator": "excluded",
"type": "exists"
},
{
"field": "host.name",
"operator": "included",
"type": "match_any",
"value": ["some host", "another host"]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"list_id": "detection_list_3",
"item_id": "simple_list_item_three_non-value_list",
"tags": [
"user added string for a tag",
"malware"
],
"type": "simple",
"description": "This is a sample exception list item with two non-value list entries",
"name": "Sample Detection Exception List Item",
"os_types": [
"windows"
],
"comments": [],
"entries": [
{
"field": "actingProcess.file.signer",
"operator": "excluded",
"type": "exists"
},
{
"field": "host.name",
"operator": "included",
"type": "match_any",
"value": ["some host", "another host"]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';
import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui';
import { History } from 'history';

import { Spacer } from '../../../../../../common/components/page';
import { NamespaceType } from '../../../../../../../../lists/common';
import { FormatUrl } from '../../../../../../common/components/link_to';
import { LinkAnchor } from '../../../../../../common/components/links';
Expand All @@ -17,15 +18,10 @@ import { ExceptionListInfo } from './use_all_exception_lists';
import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine';

export type AllExceptionListsColumns = EuiBasicTableColumn<ExceptionListInfo>;
export type Func = (arg: {
id: string;
listId: string;
namespaceType: NamespaceType;
}) => () => void;

export const getAllExceptionListsColumns = (
onExport: Func,
onDelete: Func,
onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
history: History,
formatUrl: FormatUrl
): AllExceptionListsColumns[] => [
Expand Down Expand Up @@ -64,8 +60,9 @@ export const getAllExceptionListsColumns = (
return (
<>
{value.map(({ id, name }, index) => (
<>
<Spacer key={id}>
<LinkAnchor
key={id}
data-test-subj="ruleName"
onClick={(ev: { preventDefault: () => void }) => {
ev.preventDefault();
Expand All @@ -76,7 +73,7 @@ export const getAllExceptionListsColumns = (
{name}
</LinkAnchor>
{index !== value.length - 1 ? ', ' : ''}
</>
</Spacer>
))}
</>
);
Expand Down Expand Up @@ -120,11 +117,7 @@ export const getAllExceptionListsColumns = (
render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => (
<EuiButtonIcon
color="danger"
onClick={onDelete({
id,
listId,
namespaceType,
})}
onClick={onDelete({ id, listId, namespaceType })}
aria-label="Delete exception list"
iconType="trash"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { AllRulesUtilityBar } from '../utility_bar';
import { LastUpdatedAt } from '../../../../../../common/components/last_updated';
import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns';
import { useAllExceptionLists } from './use_all_exception_lists';
import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal';
import { patchRule } from '../../../../../containers/detection_engine/rules/api';

// Known lost battle with Eui :(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -48,12 +50,33 @@ interface ExceptionListsTableProps {
formatUrl: FormatUrl;
}

interface ReferenceModalState {
contentText: string;
rulesReferences: string[];
isLoading: boolean;
listId: string;
listNamespaceType: NamespaceType;
}

const exceptionReferenceModalInitialState: ReferenceModalState = {
contentText: '',
rulesReferences: [],
isLoading: false,
listId: '',
listNamespaceType: 'single',
};

export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
({ formatUrl, history, hasNoPermissions, loading }) => {
const {
services: { http, notifications },
} = useKibana();
const { exportExceptionList } = useApi(http);
const { exportExceptionList, deleteExceptionList } = useApi(http);

const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false);
const [referenceModalState, setReferenceModalState] = useState<ReferenceModalState>(
exceptionReferenceModalInitialState
);
const [filters, setFilters] = useState<ExceptionListFilter>({
name: null,
list_id: null,
Expand All @@ -67,15 +90,36 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
notifications,
showTrustedApps: false,
});
const [loadingTableInfo, data] = useAllExceptionLists({
exceptionLists: exceptions ?? [],
});
const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists(
{
exceptionLists: exceptions ?? [],
}
);
const [initLoading, setInitLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState(Date.now());
const [deletingListIds, setDeletingListIds] = useState<string[]>([]);
const [exportingListIds, setExportingListIds] = useState<string[]>([]);
const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({});

const handleDeleteSuccess = useCallback(
(listId?: string) => () => {
notifications.toasts.addSuccess({
title: i18n.exceptionDeleteSuccessMessage(listId ?? referenceModalState.listId),
});
},
[notifications.toasts, referenceModalState.listId]
);

const handleDeleteError = useCallback(
(err: Error & { body?: { message: string } }): void => {
notifications.toasts.addError(err, {
title: i18n.EXCEPTION_DELETE_ERROR,
toastMessage: err.body != null ? err.body.message : err.message,
});
},
[notifications.toasts]
);

const handleDelete = useCallback(
({
id,
Expand All @@ -88,14 +132,45 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
}) => async () => {
try {
setDeletingListIds((ids) => [...ids, id]);
if (refreshExceptions != null) {
await refreshExceptions();
}

if (exceptionsListsRef[id] != null && exceptionsListsRef[id].rules.length === 0) {
await deleteExceptionList({
id,
namespaceType,
onError: handleDeleteError,
onSuccess: handleDeleteSuccess(listId),
});

if (refreshExceptions != null) {
refreshExceptions();
}
} else {
setReferenceModalState({
contentText: i18n.referenceErrorMessage(exceptionsListsRef[id].rules.length),
rulesReferences: exceptionsListsRef[id].rules.map(({ name }) => name),
isLoading: true,
listId: id,
yctercero marked this conversation as resolved.
Show resolved Hide resolved
listNamespaceType: namespaceType,
});
setShowReferenceErrorModal(true);
}
// route to patch rules with associated exception list
} catch (error) {
notifications.toasts.addError(error, { title: i18n.EXCEPTION_DELETE_ERROR });
handleDeleteError(error);
} finally {
setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]);
}
},
[notifications.toasts]
[
deleteExceptionList,
exceptionsListsRef,
handleDeleteError,
handleDeleteSuccess,
refreshExceptions,
]
);

const handleExportSuccess = useCallback(
Expand Down Expand Up @@ -182,6 +257,67 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
setFilters(formattedFilter);
}, []);

const handleCloseReferenceErrorModal = useCallback((): void => {
setDeletingListIds([]);
setShowReferenceErrorModal(false);
setReferenceModalState({
contentText: '',
rulesReferences: [],
isLoading: false,
listId: '',
listNamespaceType: 'single',
});
}, []);

const handleReferenceDelete = useCallback(async (): Promise<void> => {
const exceptionListId = referenceModalState.listId;
const exceptionListNamespaceType = referenceModalState.listNamespaceType;
const relevantRules = exceptionsListsRef[exceptionListId].rules;

try {
await Promise.all(
relevantRules.map((rule) => {
const abortCtrl = new AbortController();
const exceptionLists = (rule.exceptions_list ?? []).filter(
({ id }) => id !== exceptionListId
);

return patchRule({
ruleProperties: {
rule_id: rule.rule_id,
exceptions_list: exceptionLists,
},
signal: abortCtrl.signal,
});
})
);

await deleteExceptionList({
id: exceptionListId,
namespaceType: exceptionListNamespaceType,
onError: handleDeleteError,
onSuccess: handleDeleteSuccess(),
});
} catch (err) {
handleDeleteError(err);
} finally {
setReferenceModalState(exceptionReferenceModalInitialState);
setDeletingListIds([]);
setShowReferenceErrorModal(false);
if (refreshExceptions != null) {
refreshExceptions();
}
}
}, [
referenceModalState.listId,
referenceModalState.listNamespaceType,
exceptionsListsRef,
deleteExceptionList,
handleDeleteError,
handleDeleteSuccess,
refreshExceptions,
]);

const paginationMemo = useMemo(
() => ({
pageIndex: pagination.page - 1,
Expand All @@ -196,19 +332,14 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
setExportDownload({});
}, []);

const tableItems = (data ?? []).map((item) => ({
const tableItems = (exceptionListsWithRuleRefs ?? []).map((item) => ({
...item,
isDeleting: deletingListIds.includes(item.id),
isExporting: exportingListIds.includes(item.id),
}));

return (
<>
<AutoDownload
blob={exportDownload.blob}
name={`${exportDownload.name}.ndjson`}
onDownload={handleOnDownload}
/>
<Panel loading={!initLoading && loadingTableInfo} data-test-subj="allExceptionListsPanel">
<>
{loadingTableInfo && (
Expand All @@ -235,7 +366,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
/>
</HeaderSection>

{loadingTableInfo && !initLoading && (
{loadingTableInfo && !initLoading && !showReferenceErrorModal && (
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
)}
{initLoading ? (
Expand All @@ -245,7 +376,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
<AllRulesUtilityBar
showBulkActions={false}
userHasNoPermissions={hasNoPermissions}
paginationTotal={data.length ?? 0}
paginationTotal={exceptionListsWithRuleRefs.length ?? 0}
numberSelectedItems={0}
onRefresh={handleRefresh}
/>
Expand All @@ -263,9 +394,23 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
)}
</>
</Panel>
<AutoDownload
blob={exportDownload.blob}
name={`${exportDownload.name}.ndjson`}
onDownload={handleOnDownload}
/>
<ReferenceErrorModal
cancelText={i18n.REFERENCE_MODAL_CANCEL_BUTTON}
confirmText={i18n.REFERENCE_MODAL_CONFIRM_BUTTON}
contentText={referenceModalState.contentText}
onCancel={handleCloseReferenceErrorModal}
onClose={handleCloseReferenceErrorModal}
onConfirm={handleReferenceDelete}
references={referenceModalState.rulesReferences}
showModal={showReferenceErrorModal}
titleText={i18n.REFERENCE_MODAL_TITLE}
/>
</>
);
}
);

ExceptionListsTable.displayName = 'ExceptionListsTable';
Loading