Skip to content

Commit

Permalink
[Security Solution][Detections] - Fix export on exceptions list view (#…
Browse files Browse the repository at this point in the history
…86135) (#86862)

## 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.

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
yctercero and kibanamachine authored Dec 23, 2020
1 parent 19a0119 commit fa0e310
Show file tree
Hide file tree
Showing 18 changed files with 459 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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({});
});
});
Original file line number Diff line number Diff line change
@@ -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>;
1 change: 1 addition & 0 deletions x-pack/plugins/lists/common/schemas/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
47 changes: 47 additions & 0 deletions x-pack/plugins/lists/public/exceptions/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
addExceptionListItem,
deleteExceptionListById,
deleteExceptionListItemById,
exportExceptionList,
fetchExceptionListById,
fetchExceptionListItemById,
fetchExceptionLists,
Expand Down Expand Up @@ -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);
});
});
});
25 changes: 25 additions & 0 deletions x-pack/plugins/lists/public/exceptions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
ApiCallByIdProps,
ApiCallByListIdProps,
ApiCallFetchExceptionListsProps,
ExportExceptionListProps,
UpdateExceptionListItemProps,
UpdateExceptionListProps,
} from './types';
Expand Down Expand Up @@ -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,
});
25 changes: 24 additions & 1 deletion x-pack/plugins/lists/public/exceptions/hooks/use_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 => {
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions x-pack/plugins/lists/public/exceptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -156,3 +167,11 @@ export interface AddEndpointExceptionListProps {
http: HttpStart;
signal: AbortSignal;
}

export interface ExportExceptionListProps {
http: HttpStart;
id: string;
listId: string;
namespaceType: NamespaceType;
signal: AbortSignal;
}
Loading

0 comments on commit fa0e310

Please sign in to comment.