diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.mock.ts new file mode 100644 index 0000000000000..4e6655ec1d1d6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.mock.ts @@ -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, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts new file mode 100644 index 0000000000000..6af7f6323c135 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.test.ts @@ -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({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts new file mode 100644 index 0000000000000..b5061e903a824 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts @@ -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; diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts index 172d73a5c7377..36e41bf52aa40 100644 --- a/x-pack/plugins/lists/common/schemas/request/index.ts +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -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'; diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index e45403e319c29..7570e1f050abb 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -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); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index fc0c8934d6397..f7032c22cb6c2 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -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 => + http.fetch(`${EXCEPTION_LIST_URL}/_export`, { + method: 'GET', + query: { id, list_id: listId, namespace_type: namespaceType }, + signal, + }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts index def2f2626b8ec..31a8d3ac5f598 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts @@ -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; getExceptionListsItems: (arg: ApiCallFindListsItemsMemoProps) => Promise; + exportExceptionList: (arg: ApiListExportProps) => Promise; } 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 { + 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, diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index 02b78bc1a5e58..6a238e22344b6 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -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; +} diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts new file mode 100644 index 0000000000000..1394bf48cd2c7 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -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` }; +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index 0d99d726d232d..a1a54a88c0ed0 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -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'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index 163126f1277c1..1f29d0aaeeb48 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -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); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.test.tsx b/x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.test.tsx rename to x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.test.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.tsx b/x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/auto_download.tsx rename to x-pack/plugins/security_solution/public/common/components/auto_download/auto_download.tsx diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index f0e47fcd5c104..57c4eee95cd8c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -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; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 57b86119dc164..79cfd53a4fa00 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -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; -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) => ( @@ -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) => ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 65aaaea06b40f..ac9c558022c26 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -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( const { services: { http, notifications }, } = useKibana(); + const { exportExceptionList } = useApi(http); const [filters, setFilters] = useState({ name: null, list_id: null, @@ -69,10 +72,67 @@ export const ExceptionListsTable = React.memo( }); const [initLoading, setInitLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(Date.now()); + const [deletingListIds, setDeletingListIds] = useState([]); + const [exportingListIds, setExportingListIds] = useState([]); + 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( setFilters(formattedFilter); }, []); - const handleSearchChange = useCallback( - (event: ChangeEvent) => { - const val = event.target.value; - handleSearch(val); - }, - [handleSearch] - ); - const paginationMemo = useMemo( () => ({ pageIndex: pagination.page - 1, @@ -140,8 +192,23 @@ export const ExceptionListsTable = React.memo( [pagination] ); + const handleOnDownload = useCallback(() => { + setExportDownload({}); + }, []); + + const tableItems = (data ?? []).map((item) => ({ + ...item, + isDeleting: deletingListIds.includes(item.id), + isExporting: exportingListIds.includes(item.id), + })); + return ( <> + <> {loadingTableInfo && ( @@ -162,7 +229,6 @@ export const ExceptionListsTable = React.memo( 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( columns={exceptionsColumns} isSelectable={!hasNoPermissions ?? false} itemId="id" - items={data ?? []} + items={tableItems} noItemsMessage={emptyPrompt} onChange={() => {}} pagination={paginationMemo} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts index 2eba8fb2e579b..7483b8e943d30 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx index 4b47080cc2da1..3f343da605213 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/use_all_exception_lists.tsx @@ -61,7 +61,7 @@ export const useAllExceptionLists = ({ const { data: rules } = await fetchRules({ pagination: { page: 1, - perPage: 500, + perPage: 10000, total: 0, }, signal: abortCtrl.signal,