Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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",
"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 @@ -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,28 @@ 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(() => {
notifications.toasts.addSuccess({ title: i18n.EXCEPTION_DELETE_SUCCESS });
}, [notifications.toasts]);

const handleDeleteError = useCallback(
(err: Error) => {
notifications.toasts.addError(err, { title: i18n.EXCEPTION_DELETE_ERROR });
},
[notifications.toasts]
);

const handleDelete = useCallback(
({
id,
Expand All @@ -88,14 +124,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,
});

if (refreshExceptions != null) {
refreshExceptions();
}
} else {
setReferenceModalState({
contentText: i18n.referenceErrorMessage(exceptionsListsRef[id].rules.length),
rulesReferences: exceptionsListsRef[id].rules.map(({ name }) => name),
isLoading: true,
listId: id,
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 +249,68 @@ 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) {
notifications.toasts.addError(err, { title: i18n.EXCEPTION_DELETE_ERROR });
} finally {
setReferenceModalState(exceptionReferenceModalInitialState);
setDeletingListIds([]);
setShowReferenceErrorModal(false);
if (refreshExceptions != null) {
refreshExceptions();
}
}
}, [
referenceModalState.listId,
referenceModalState.listNamespaceType,
exceptionsListsRef,
deleteExceptionList,
handleDeleteError,
handleDeleteSuccess,
notifications.toasts,
refreshExceptions,
]);

const paginationMemo = useMemo(
() => ({
pageIndex: pagination.page - 1,
Expand All @@ -196,19 +325,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 +359,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 +369,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 +387,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';
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,38 @@ export const EXCEPTION_DELETE_ERROR = i18n.translate(
defaultMessage: 'Error occurred deleting exception list',
}
);

export const EXCEPTION_DELETE_SUCCESS = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.all.exceptions.deleteSuccess',
{
defaultMessage: 'Exception list deleted successfully',
}
);

export const REFERENCE_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.exceptions.referenceModalTitle',
{
defaultMessage: 'Remove exception list',
}
);

export const REFERENCE_MODAL_CANCEL_BUTTON = i18n.translate(
'xpack.securitySolution.exceptions.referenceModalCancelButton',
{
defaultMessage: 'Cancel',
}
);

export const REFERENCE_MODAL_CONFIRM_BUTTON = i18n.translate(
'xpack.securitySolution.exceptions.referenceModalDeleteButton',
{
defaultMessage: 'Remove exceptions list',
}
);

export const referenceErrorMessage = (referenceCount: number) =>
i18n.translate('xpack.securitySolution.exceptions.referenceModalDescription', {
defaultMessage:
'This exceptions list is associated with ({referenceCount}) {referenceCount, plural, =1 {rule} other {rules}}. Removing this exception list will also remove its reference from the associated rules.',
values: { referenceCount },
});
Loading