Skip to content

Commit 391e082

Browse files
committed
[Security Solutions][Detections] - Fix exception list table referential deletion (#87231)
### Summary This PR concentrates on fixing the deletion on the exceptions list table view. This fix is intermediary and a more thorough, backend solution is needed. Currently, if you delete an exception list, it deletes the exception list SO, but does not remove references to it from rules. This PR allows for a quick fix conducting this logic client side.
1 parent 980035c commit 391e082

10 files changed

Lines changed: 339 additions & 71 deletions

File tree

x-pack/plugins/lists/public/exceptions/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export interface ApiCallByIdProps {
8686
export interface ApiCallMemoProps {
8787
id: string;
8888
namespaceType: NamespaceType;
89-
onError: (arg: string[]) => void;
89+
onError: (arg: Error) => void;
9090
onSuccess: () => void;
9191
}
9292

x-pack/plugins/lists/server/scripts/exception_lists/new/references/exception_list_item_1_non_value_list.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"list_id": "detection_list_1",
3-
"item_id": "simple_list_item_two_non-value_list",
3+
"item_id": "simple_list_item_one_non-value_list",
44
"tags": [
55
"user added string for a tag",
66
"malware"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"list_id": "detection_list_2",
3+
"item_id": "simple_list_item_two_non-value_list",
4+
"tags": [
5+
"user added string for a tag",
6+
"malware"
7+
],
8+
"type": "simple",
9+
"description": "This is a sample exception list item with two non-value list entries",
10+
"name": "Sample Detection Exception List Item",
11+
"os_types": [
12+
"windows"
13+
],
14+
"comments": [],
15+
"entries": [
16+
{
17+
"field": "actingProcess.file.signer",
18+
"operator": "excluded",
19+
"type": "exists"
20+
},
21+
{
22+
"field": "host.name",
23+
"operator": "included",
24+
"type": "match_any",
25+
"value": ["some host", "another host"]
26+
}
27+
]
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"list_id": "detection_list_3",
3+
"item_id": "simple_list_item_three_non-value_list",
4+
"tags": [
5+
"user added string for a tag",
6+
"malware"
7+
],
8+
"type": "simple",
9+
"description": "This is a sample exception list item with two non-value list entries",
10+
"name": "Sample Detection Exception List Item",
11+
"os_types": [
12+
"windows"
13+
],
14+
"comments": [],
15+
"entries": [
16+
{
17+
"field": "actingProcess.file.signer",
18+
"operator": "excluded",
19+
"type": "exists"
20+
},
21+
{
22+
"field": "host.name",
23+
"operator": "included",
24+
"type": "match_any",
25+
"value": ["some host", "another host"]
26+
}
27+
]
28+
}

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import React from 'react';
99
import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui';
1010
import { History } from 'history';
1111

12+
import { Spacer } from '../../../../../../common/components/page';
1213
import { NamespaceType } from '../../../../../../../../lists/common';
1314
import { FormatUrl } from '../../../../../../common/components/link_to';
1415
import { LinkAnchor } from '../../../../../../common/components/links';
@@ -17,15 +18,10 @@ import { ExceptionListInfo } from './use_all_exception_lists';
1718
import { getRuleDetailsUrl } from '../../../../../../common/components/link_to/redirect_to_detection_engine';
1819

1920
export type AllExceptionListsColumns = EuiBasicTableColumn<ExceptionListInfo>;
20-
export type Func = (arg: {
21-
id: string;
22-
listId: string;
23-
namespaceType: NamespaceType;
24-
}) => () => void;
2521

2622
export const getAllExceptionListsColumns = (
27-
onExport: Func,
28-
onDelete: Func,
23+
onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
24+
onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
2925
history: History,
3026
formatUrl: FormatUrl
3127
): AllExceptionListsColumns[] => [
@@ -64,8 +60,9 @@ export const getAllExceptionListsColumns = (
6460
return (
6561
<>
6662
{value.map(({ id, name }, index) => (
67-
<>
63+
<Spacer key={id}>
6864
<LinkAnchor
65+
key={id}
6966
data-test-subj="ruleName"
7067
onClick={(ev: { preventDefault: () => void }) => {
7168
ev.preventDefault();
@@ -76,7 +73,7 @@ export const getAllExceptionListsColumns = (
7673
{name}
7774
</LinkAnchor>
7875
{index !== value.length - 1 ? ', ' : ''}
79-
</>
76+
</Spacer>
8077
))}
8178
</>
8279
);
@@ -120,11 +117,7 @@ export const getAllExceptionListsColumns = (
120117
render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => (
121118
<EuiButtonIcon
122119
color="danger"
123-
onClick={onDelete({
124-
id,
125-
listId,
126-
namespaceType,
127-
})}
120+
onClick={onDelete({ id, listId, namespaceType })}
128121
aria-label="Delete exception list"
129122
iconType="trash"
130123
/>

x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx

Lines changed: 161 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { AllRulesUtilityBar } from '../utility_bar';
2929
import { LastUpdatedAt } from '../../../../../../common/components/last_updated';
3030
import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns';
3131
import { useAllExceptionLists } from './use_all_exception_lists';
32+
import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal';
33+
import { patchRule } from '../../../../../containers/detection_engine/rules/api';
3234

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

53+
interface ReferenceModalState {
54+
contentText: string;
55+
rulesReferences: string[];
56+
isLoading: boolean;
57+
listId: string;
58+
listNamespaceType: NamespaceType;
59+
}
60+
61+
const exceptionReferenceModalInitialState: ReferenceModalState = {
62+
contentText: '',
63+
rulesReferences: [],
64+
isLoading: false,
65+
listId: '',
66+
listNamespaceType: 'single',
67+
};
68+
5169
export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
5270
({ formatUrl, history, hasNoPermissions, loading }) => {
5371
const {
5472
services: { http, notifications },
5573
} = useKibana();
56-
const { exportExceptionList } = useApi(http);
74+
const { exportExceptionList, deleteExceptionList } = useApi(http);
75+
76+
const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false);
77+
const [referenceModalState, setReferenceModalState] = useState<ReferenceModalState>(
78+
exceptionReferenceModalInitialState
79+
);
5780
const [filters, setFilters] = useState<ExceptionListFilter>({
5881
name: null,
5982
list_id: null,
@@ -67,15 +90,36 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
6790
notifications,
6891
showTrustedApps: false,
6992
});
70-
const [loadingTableInfo, data] = useAllExceptionLists({
71-
exceptionLists: exceptions ?? [],
72-
});
93+
const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists(
94+
{
95+
exceptionLists: exceptions ?? [],
96+
}
97+
);
7398
const [initLoading, setInitLoading] = useState(true);
7499
const [lastUpdated, setLastUpdated] = useState(Date.now());
75100
const [deletingListIds, setDeletingListIds] = useState<string[]>([]);
76101
const [exportingListIds, setExportingListIds] = useState<string[]>([]);
77102
const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({});
78103

104+
const handleDeleteSuccess = useCallback(
105+
(listId?: string) => () => {
106+
notifications.toasts.addSuccess({
107+
title: i18n.exceptionDeleteSuccessMessage(listId ?? referenceModalState.listId),
108+
});
109+
},
110+
[notifications.toasts, referenceModalState.listId]
111+
);
112+
113+
const handleDeleteError = useCallback(
114+
(err: Error & { body?: { message: string } }): void => {
115+
notifications.toasts.addError(err, {
116+
title: i18n.EXCEPTION_DELETE_ERROR,
117+
toastMessage: err.body != null ? err.body.message : err.message,
118+
});
119+
},
120+
[notifications.toasts]
121+
);
122+
79123
const handleDelete = useCallback(
80124
({
81125
id,
@@ -88,14 +132,45 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
88132
}) => async () => {
89133
try {
90134
setDeletingListIds((ids) => [...ids, id]);
135+
if (refreshExceptions != null) {
136+
await refreshExceptions();
137+
}
138+
139+
if (exceptionsListsRef[id] != null && exceptionsListsRef[id].rules.length === 0) {
140+
await deleteExceptionList({
141+
id,
142+
namespaceType,
143+
onError: handleDeleteError,
144+
onSuccess: handleDeleteSuccess(listId),
145+
});
146+
147+
if (refreshExceptions != null) {
148+
refreshExceptions();
149+
}
150+
} else {
151+
setReferenceModalState({
152+
contentText: i18n.referenceErrorMessage(exceptionsListsRef[id].rules.length),
153+
rulesReferences: exceptionsListsRef[id].rules.map(({ name }) => name),
154+
isLoading: true,
155+
listId: id,
156+
listNamespaceType: namespaceType,
157+
});
158+
setShowReferenceErrorModal(true);
159+
}
91160
// route to patch rules with associated exception list
92161
} catch (error) {
93-
notifications.toasts.addError(error, { title: i18n.EXCEPTION_DELETE_ERROR });
162+
handleDeleteError(error);
94163
} finally {
95164
setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]);
96165
}
97166
},
98-
[notifications.toasts]
167+
[
168+
deleteExceptionList,
169+
exceptionsListsRef,
170+
handleDeleteError,
171+
handleDeleteSuccess,
172+
refreshExceptions,
173+
]
99174
);
100175

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

260+
const handleCloseReferenceErrorModal = useCallback((): void => {
261+
setDeletingListIds([]);
262+
setShowReferenceErrorModal(false);
263+
setReferenceModalState({
264+
contentText: '',
265+
rulesReferences: [],
266+
isLoading: false,
267+
listId: '',
268+
listNamespaceType: 'single',
269+
});
270+
}, []);
271+
272+
const handleReferenceDelete = useCallback(async (): Promise<void> => {
273+
const exceptionListId = referenceModalState.listId;
274+
const exceptionListNamespaceType = referenceModalState.listNamespaceType;
275+
const relevantRules = exceptionsListsRef[exceptionListId].rules;
276+
277+
try {
278+
await Promise.all(
279+
relevantRules.map((rule) => {
280+
const abortCtrl = new AbortController();
281+
const exceptionLists = (rule.exceptions_list ?? []).filter(
282+
({ id }) => id !== exceptionListId
283+
);
284+
285+
return patchRule({
286+
ruleProperties: {
287+
rule_id: rule.rule_id,
288+
exceptions_list: exceptionLists,
289+
},
290+
signal: abortCtrl.signal,
291+
});
292+
})
293+
);
294+
295+
await deleteExceptionList({
296+
id: exceptionListId,
297+
namespaceType: exceptionListNamespaceType,
298+
onError: handleDeleteError,
299+
onSuccess: handleDeleteSuccess(),
300+
});
301+
} catch (err) {
302+
handleDeleteError(err);
303+
} finally {
304+
setReferenceModalState(exceptionReferenceModalInitialState);
305+
setDeletingListIds([]);
306+
setShowReferenceErrorModal(false);
307+
if (refreshExceptions != null) {
308+
refreshExceptions();
309+
}
310+
}
311+
}, [
312+
referenceModalState.listId,
313+
referenceModalState.listNamespaceType,
314+
exceptionsListsRef,
315+
deleteExceptionList,
316+
handleDeleteError,
317+
handleDeleteSuccess,
318+
refreshExceptions,
319+
]);
320+
185321
const paginationMemo = useMemo(
186322
() => ({
187323
pageIndex: pagination.page - 1,
@@ -196,19 +332,14 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
196332
setExportDownload({});
197333
}, []);
198334

199-
const tableItems = (data ?? []).map((item) => ({
335+
const tableItems = (exceptionListsWithRuleRefs ?? []).map((item) => ({
200336
...item,
201337
isDeleting: deletingListIds.includes(item.id),
202338
isExporting: exportingListIds.includes(item.id),
203339
}));
204340

205341
return (
206342
<>
207-
<AutoDownload
208-
blob={exportDownload.blob}
209-
name={`${exportDownload.name}.ndjson`}
210-
onDownload={handleOnDownload}
211-
/>
212343
<Panel loading={!initLoading && loadingTableInfo} data-test-subj="allExceptionListsPanel">
213344
<>
214345
{loadingTableInfo && (
@@ -235,7 +366,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
235366
/>
236367
</HeaderSection>
237368

238-
{loadingTableInfo && !initLoading && (
369+
{loadingTableInfo && !initLoading && !showReferenceErrorModal && (
239370
<Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" />
240371
)}
241372
{initLoading ? (
@@ -245,7 +376,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
245376
<AllRulesUtilityBar
246377
showBulkActions={false}
247378
userHasNoPermissions={hasNoPermissions}
248-
paginationTotal={data.length ?? 0}
379+
paginationTotal={exceptionListsWithRuleRefs.length ?? 0}
249380
numberSelectedItems={0}
250381
onRefresh={handleRefresh}
251382
/>
@@ -263,9 +394,23 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
263394
)}
264395
</>
265396
</Panel>
397+
<AutoDownload
398+
blob={exportDownload.blob}
399+
name={`${exportDownload.name}.ndjson`}
400+
onDownload={handleOnDownload}
401+
/>
402+
<ReferenceErrorModal
403+
cancelText={i18n.REFERENCE_MODAL_CANCEL_BUTTON}
404+
confirmText={i18n.REFERENCE_MODAL_CONFIRM_BUTTON}
405+
contentText={referenceModalState.contentText}
406+
onCancel={handleCloseReferenceErrorModal}
407+
onClose={handleCloseReferenceErrorModal}
408+
onConfirm={handleReferenceDelete}
409+
references={referenceModalState.rulesReferences}
410+
showModal={showReferenceErrorModal}
411+
titleText={i18n.REFERENCE_MODAL_TITLE}
412+
/>
266413
</>
267414
);
268415
}
269416
);
270-
271-
ExceptionListsTable.displayName = 'ExceptionListsTable';

0 commit comments

Comments
 (0)