mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Detections] - Fix export on exceptions list view (#86135)
## Summary This PR addresses a fix on the exceptions list table export functionality. A dedicated route for exception list export needed to be created. List is exported into an `.ndjson` format. Exception lists consist of two elements - the list itself, and its items. The export file should now contain both these elements, the list followed by its items.
This commit is contained in:
parent
35b10b5354
commit
3dfb1aba2a
18 changed files with 459 additions and 23 deletions
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ID, LIST_ID, NAMESPACE_TYPE } from '../../constants.mock';
|
||||
|
||||
import { ExportExceptionListQuerySchema } from './export_exception_list_query_schema';
|
||||
|
||||
export const getExportExceptionListQuerySchemaMock = (): ExportExceptionListQuerySchema => ({
|
||||
id: ID,
|
||||
list_id: LIST_ID,
|
||||
namespace_type: NAMESPACE_TYPE,
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports';
|
||||
|
||||
import {
|
||||
ExportExceptionListQuerySchema,
|
||||
exportExceptionListQuerySchema,
|
||||
} from './export_exception_list_query_schema';
|
||||
import { getExportExceptionListQuerySchemaMock } from './export_exception_list_query_schema.mock';
|
||||
|
||||
describe('export_exception_list_schema', () => {
|
||||
test('it should validate a typical lists request', () => {
|
||||
const payload = getExportExceptionListQuerySchemaMock();
|
||||
const decoded = exportExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should NOT accept an undefined for an id', () => {
|
||||
const payload = getExportExceptionListQuerySchemaMock();
|
||||
// @ts-expect-error
|
||||
delete payload.id;
|
||||
const decoded = exportExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should default namespace_type to "single" if an undefined given for namespacetype', () => {
|
||||
const payload = getExportExceptionListQuerySchemaMock();
|
||||
delete payload.namespace_type;
|
||||
const decoded = exportExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(message.schema).toEqual({
|
||||
id: 'uuid_here',
|
||||
list_id: 'some-list-id',
|
||||
namespace_type: 'single',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should NOT accept an undefined for an list_id', () => {
|
||||
const payload = getExportExceptionListQuerySchemaMock();
|
||||
// @ts-expect-error
|
||||
delete payload.list_id;
|
||||
const decoded = exportExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "list_id"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not allow an extra key to be sent in', () => {
|
||||
const payload: ExportExceptionListQuerySchema & {
|
||||
extraKey?: string;
|
||||
} = getExportExceptionListQuerySchemaMock();
|
||||
payload.extraKey = 'some new value';
|
||||
const decoded = exportExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { id, list_id, namespace_type } from '../common/schemas';
|
||||
|
||||
export const exportExceptionListQuerySchema = t.exact(
|
||||
t.type({
|
||||
id,
|
||||
list_id,
|
||||
namespace_type,
|
||||
// TODO: Add file_name here with a default value
|
||||
})
|
||||
);
|
||||
|
||||
export type ExportExceptionListQuerySchema = t.OutputOf<typeof exportExceptionListQuerySchema>;
|
|
@ -14,6 +14,7 @@ export * from './delete_exception_list_item_schema';
|
|||
export * from './delete_exception_list_schema';
|
||||
export * from './delete_list_item_schema';
|
||||
export * from './delete_list_schema';
|
||||
export * from './export_exception_list_query_schema';
|
||||
export * from './export_list_item_query_schema';
|
||||
export * from './find_endpoint_list_item_schema';
|
||||
export * from './find_exception_list_item_schema';
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
addExceptionListItem,
|
||||
deleteExceptionListById,
|
||||
deleteExceptionListItemById,
|
||||
exportExceptionList,
|
||||
fetchExceptionListById,
|
||||
fetchExceptionListItemById,
|
||||
fetchExceptionLists,
|
||||
|
@ -870,4 +871,50 @@ describe('Exceptions Lists API', () => {
|
|||
expect(exceptionResponse).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#exportExceptionList', () => {
|
||||
const blob: Blob = {
|
||||
arrayBuffer: jest.fn(),
|
||||
size: 89,
|
||||
slice: jest.fn(),
|
||||
stream: jest.fn(),
|
||||
text: jest.fn(),
|
||||
type: 'json',
|
||||
} as Blob;
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock.fetch.mockResolvedValue(blob);
|
||||
});
|
||||
|
||||
test('it invokes "exportExceptionList" with expected url and body values', async () => {
|
||||
await exportExceptionList({
|
||||
http: httpMock,
|
||||
id: 'some-id',
|
||||
listId: 'list-id',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/_export', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
id: 'some-id',
|
||||
list_id: 'list-id',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns expected list to export on success', async () => {
|
||||
const exceptionResponse = await exportExceptionList({
|
||||
http: httpMock,
|
||||
id: 'some-id',
|
||||
listId: 'list-id',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(blob);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
ApiCallByIdProps,
|
||||
ApiCallByListIdProps,
|
||||
ApiCallFetchExceptionListsProps,
|
||||
ExportExceptionListProps,
|
||||
UpdateExceptionListItemProps,
|
||||
UpdateExceptionListProps,
|
||||
} from './types';
|
||||
|
@ -537,3 +538,27 @@ export const addEndpointExceptionList = async ({
|
|||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch an ExceptionList by providing a ExceptionList ID
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param id ExceptionList ID (not list_id)
|
||||
* @param listId ExceptionList LIST_ID (not id)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const exportExceptionList = async ({
|
||||
http,
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
signal,
|
||||
}: ExportExceptionListProps): Promise<Blob> =>
|
||||
http.fetch<Blob>(`${EXCEPTION_LIST_URL}/_export`, {
|
||||
method: 'GET',
|
||||
query: { id, list_id: listId, namespace_type: namespaceType },
|
||||
signal,
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
|||
import * as Api from '../api';
|
||||
import { HttpStart } from '../../../../../../src/core/public';
|
||||
import { ExceptionListItemSchema, ExceptionListSchema } from '../../../common/schemas';
|
||||
import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps } from '../types';
|
||||
import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps, ApiListExportProps } from '../types';
|
||||
import { getIdsAndNamespaces } from '../utils';
|
||||
|
||||
export interface ExceptionsApi {
|
||||
|
@ -22,6 +22,7 @@ export interface ExceptionsApi {
|
|||
arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListSchema) => void }
|
||||
) => Promise<void>;
|
||||
getExceptionListsItems: (arg: ApiCallFindListsItemsMemoProps) => Promise<void>;
|
||||
exportExceptionList: (arg: ApiListExportProps) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useApi = (http: HttpStart): ExceptionsApi => {
|
||||
|
@ -67,6 +68,28 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
|
|||
onError(error);
|
||||
}
|
||||
},
|
||||
async exportExceptionList({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: ApiListExportProps): Promise<void> {
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
try {
|
||||
const blob = await Api.exportExceptionList({
|
||||
http,
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
onSuccess(blob);
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
},
|
||||
async getExceptionItem({
|
||||
id,
|
||||
namespaceType,
|
||||
|
|
|
@ -90,6 +90,17 @@ export interface ApiCallMemoProps {
|
|||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// TODO: Switch to use ApiCallMemoProps
|
||||
// after cleaning up exceptions/api file to
|
||||
// remove unnecessary validation checks
|
||||
export interface ApiListExportProps {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
onError: (err: Error) => void;
|
||||
onSuccess: (blob: Blob) => void;
|
||||
}
|
||||
|
||||
export interface ApiCallFindListsItemsMemoProps {
|
||||
lists: ExceptionListIdentifiers[];
|
||||
filterOptions: FilterExceptionsOptions[];
|
||||
|
@ -156,3 +167,11 @@ export interface AddEndpointExceptionListProps {
|
|||
http: HttpStart;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ExportExceptionListProps {
|
||||
http: HttpStart;
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IRouter } from 'kibana/server';
|
||||
|
||||
import { EXCEPTION_LIST_URL } from '../../common/constants';
|
||||
import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps';
|
||||
import { exportExceptionListQuerySchema } from '../../common/schemas';
|
||||
|
||||
import { getExceptionListClient } from './utils';
|
||||
|
||||
export const exportExceptionListRoute = (router: IRouter): void => {
|
||||
router.get(
|
||||
{
|
||||
options: {
|
||||
tags: ['access:lists-read'],
|
||||
},
|
||||
path: `${EXCEPTION_LIST_URL}/_export`,
|
||||
validate: {
|
||||
query: buildRouteValidation(exportExceptionListQuerySchema),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
try {
|
||||
const { id, list_id: listId, namespace_type: namespaceType } = request.query;
|
||||
const exceptionLists = getExceptionListClient(context);
|
||||
const exceptionList = await exceptionLists.getExceptionList({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
});
|
||||
|
||||
if (exceptionList == null) {
|
||||
return siemResponse.error({
|
||||
body: `list_id: ${listId} does not exist`,
|
||||
statusCode: 400,
|
||||
});
|
||||
} else {
|
||||
const { exportData: exportList } = getExport([exceptionList]);
|
||||
const listItems = await exceptionLists.findExceptionListItem({
|
||||
filter: undefined,
|
||||
listId,
|
||||
namespaceType,
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sortField: 'exception-list.created_at',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
const { exportData: exportListItems, exportDetails } = getExport(listItems?.data ?? []);
|
||||
|
||||
const responseBody = [
|
||||
exportList,
|
||||
exportListItems,
|
||||
{ exception_list_items_details: exportDetails },
|
||||
];
|
||||
|
||||
// TODO: Allow the API to override the name of the file to export
|
||||
const fileName = exceptionList.list_id;
|
||||
return response.ok({
|
||||
body: transformDataToNdjson(responseBody),
|
||||
headers: {
|
||||
'Content-Disposition': `attachment; filename="${fileName}"`,
|
||||
'Content-Type': 'application/ndjson',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const transformDataToNdjson = (data: unknown[]): string => {
|
||||
if (data.length !== 0) {
|
||||
const dataString = data.map((dataItem) => JSON.stringify(dataItem)).join('\n');
|
||||
return `${dataString}\n`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getExport = (
|
||||
data: unknown[]
|
||||
): {
|
||||
exportData: string;
|
||||
exportDetails: string;
|
||||
} => {
|
||||
const ndjson = transformDataToNdjson(data);
|
||||
const exportDetails = JSON.stringify({
|
||||
exported_count: data.length,
|
||||
});
|
||||
return { exportData: ndjson, exportDetails: `${exportDetails}\n` };
|
||||
};
|
|
@ -17,6 +17,7 @@ export * from './delete_exception_list_item_route';
|
|||
export * from './delete_list_index_route';
|
||||
export * from './delete_list_item_route';
|
||||
export * from './delete_list_route';
|
||||
export * from './export_exception_list_route';
|
||||
export * from './export_list_item_route';
|
||||
export * from './find_endpoint_list_item_route';
|
||||
export * from './find_exception_list_item_route';
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
deleteListIndexRoute,
|
||||
deleteListItemRoute,
|
||||
deleteListRoute,
|
||||
exportExceptionListRoute,
|
||||
exportListItemRoute,
|
||||
findEndpointListItemRoute,
|
||||
findExceptionListItemRoute,
|
||||
|
@ -76,6 +77,7 @@ export const initRoutes = (router: IRouter, config: ConfigType): void => {
|
|||
updateExceptionListRoute(router);
|
||||
deleteExceptionListRoute(router);
|
||||
findExceptionListRoute(router);
|
||||
exportExceptionListRoute(router);
|
||||
|
||||
// exception list items
|
||||
createExceptionListItemRoute(router);
|
||||
|
|
|
@ -32,8 +32,8 @@ import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
|||
import * as i18n from './translations';
|
||||
import { buildColumns } from './table_helpers';
|
||||
import { ValueListsForm } from './form';
|
||||
import { AutoDownload } from './auto_download';
|
||||
import { ReferenceErrorModal } from './reference_error_modal';
|
||||
import { AutoDownload } from '../../../common/components/auto_download/auto_download';
|
||||
|
||||
interface ValueListsModalProps {
|
||||
onClose: () => void;
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui';
|
||||
import { History } from 'history';
|
||||
|
||||
import { NamespaceType } from '../../../../../../../../lists/common';
|
||||
import { FormatUrl } from '../../../../../../common/components/link_to';
|
||||
import { LinkAnchor } from '../../../../../../common/components/links';
|
||||
import * as i18n from './translations';
|
||||
|
@ -16,7 +17,11 @@ 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 = (listId: string) => () => void;
|
||||
export type Func = (arg: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => void;
|
||||
|
||||
export const getAllExceptionListsColumns = (
|
||||
onExport: Func,
|
||||
|
@ -96,9 +101,13 @@ export const getAllExceptionListsColumns = (
|
|||
align: 'center',
|
||||
isExpander: false,
|
||||
width: '25px',
|
||||
render: (list: ExceptionListInfo) => (
|
||||
render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => (
|
||||
<EuiButtonIcon
|
||||
onClick={onExport(list.id)}
|
||||
onClick={onExport({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
})}
|
||||
aria-label="Export exception list"
|
||||
iconType="exportAction"
|
||||
/>
|
||||
|
@ -108,10 +117,14 @@ export const getAllExceptionListsColumns = (
|
|||
align: 'center',
|
||||
width: '25px',
|
||||
isExpander: false,
|
||||
render: (list: ExceptionListInfo) => (
|
||||
render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => (
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
onClick={onDelete(list.id)}
|
||||
onClick={onDelete({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
})}
|
||||
aria-label="Delete exception list"
|
||||
iconType="trash"
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useEffect, useCallback, useState, ChangeEvent } from 'react';
|
||||
import React, { useMemo, useEffect, useCallback, useState } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiEmptyPrompt,
|
||||
|
@ -16,8 +16,10 @@ import styled from 'styled-components';
|
|||
import { History } from 'history';
|
||||
import { set } from 'lodash/fp';
|
||||
|
||||
import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download';
|
||||
import { NamespaceType } from '../../../../../../../../lists/common';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
import { useExceptionLists } from '../../../../../../shared_imports';
|
||||
import { useApi, useExceptionLists } from '../../../../../../shared_imports';
|
||||
import { FormatUrl } from '../../../../../../common/components/link_to';
|
||||
import { HeaderSection } from '../../../../../../common/components/header_section';
|
||||
import { Loader } from '../../../../../../common/components/loader';
|
||||
|
@ -51,6 +53,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
|||
const {
|
||||
services: { http, notifications },
|
||||
} = useKibana();
|
||||
const { exportExceptionList } = useApi(http);
|
||||
const [filters, setFilters] = useState<ExceptionListFilter>({
|
||||
name: null,
|
||||
list_id: null,
|
||||
|
@ -69,10 +72,67 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
|||
});
|
||||
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 handleDelete = useCallback((id: string) => () => {}, []);
|
||||
const handleDelete = useCallback(
|
||||
({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => async () => {
|
||||
try {
|
||||
setDeletingListIds((ids) => [...ids, id]);
|
||||
// route to patch rules with associated exception list
|
||||
} catch (error) {
|
||||
notifications.toasts.addError(error, { title: i18n.EXCEPTION_DELETE_ERROR });
|
||||
} finally {
|
||||
setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]);
|
||||
}
|
||||
},
|
||||
[notifications.toasts]
|
||||
);
|
||||
|
||||
const handleExport = useCallback((id: string) => () => {}, []);
|
||||
const handleExportSuccess = useCallback(
|
||||
(listId: string) => (blob: Blob): void => {
|
||||
setExportDownload({ name: listId, blob });
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleExportError = useCallback(
|
||||
(err: Error) => {
|
||||
notifications.toasts.addError(err, { title: i18n.EXCEPTION_EXPORT_ERROR });
|
||||
},
|
||||
[notifications.toasts]
|
||||
);
|
||||
|
||||
const handleExport = useCallback(
|
||||
({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}: {
|
||||
id: string;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => async () => {
|
||||
setExportingListIds((ids) => [...ids, id]);
|
||||
await exportExceptionList({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
onError: handleExportError,
|
||||
onSuccess: handleExportSuccess(listId),
|
||||
});
|
||||
},
|
||||
[exportExceptionList, handleExportError, handleExportSuccess]
|
||||
);
|
||||
|
||||
const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => {
|
||||
return getAllExceptionListsColumns(handleExport, handleDelete, history, formatUrl);
|
||||
|
@ -122,14 +182,6 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
|||
setFilters(formattedFilter);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = event.target.value;
|
||||
handleSearch(val);
|
||||
},
|
||||
[handleSearch]
|
||||
);
|
||||
|
||||
const paginationMemo = useMemo(
|
||||
() => ({
|
||||
pageIndex: pagination.page - 1,
|
||||
|
@ -140,8 +192,23 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
|||
[pagination]
|
||||
);
|
||||
|
||||
const handleOnDownload = useCallback(() => {
|
||||
setExportDownload({});
|
||||
}, []);
|
||||
|
||||
const tableItems = (data ?? []).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 && (
|
||||
|
@ -162,7 +229,6 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
|||
aria-label={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER}
|
||||
placeholder={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleSearchChange}
|
||||
disabled={initLoading}
|
||||
incremental={false}
|
||||
fullWidth
|
||||
|
@ -188,7 +254,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>(
|
|||
columns={exceptionsColumns}
|
||||
isSelectable={!hasNoPermissions ?? false}
|
||||
itemId="id"
|
||||
items={data ?? []}
|
||||
items={tableItems}
|
||||
noItemsMessage={emptyPrompt}
|
||||
onChange={() => {}}
|
||||
pagination={paginationMemo}
|
||||
|
|
|
@ -35,7 +35,7 @@ export const LIST_DATE_CREATED_TITLE = i18n.translate(
|
|||
);
|
||||
|
||||
export const LIST_DATE_UPDATED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateUPdatedTitle',
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.dateUpdatedTitle',
|
||||
{
|
||||
defaultMessage: 'Last edited',
|
||||
}
|
||||
|
@ -75,3 +75,24 @@ export const NO_LISTS_BODY = i18n.translate(
|
|||
defaultMessage: "We weren't able to find any exception lists.",
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_EXPORT_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.exportSuccess',
|
||||
{
|
||||
defaultMessage: 'Exception list export success',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_EXPORT_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.exportError',
|
||||
{
|
||||
defaultMessage: 'Exception list export error',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_DELETE_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.deleteError',
|
||||
{
|
||||
defaultMessage: 'Error occurred deleting exception list',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -61,7 +61,7 @@ export const useAllExceptionLists = ({
|
|||
const { data: rules } = await fetchRules({
|
||||
pagination: {
|
||||
page: 1,
|
||||
perPage: 500,
|
||||
perPage: 10000,
|
||||
total: 0,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue