Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detections] - Fix export on exceptions list view #86135

Merged
merged 15 commits into from
Dec 23, 2020
Merged
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
yctercero marked this conversation as resolved.
Show resolved Hide resolved
})
);

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
107 changes: 70 additions & 37 deletions x-pack/plugins/lists/public/exceptions/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/reque
import { getFoundExceptionListItemSchemaMock } from '../../common/schemas/response/found_exception_list_item_schema.mock';
import { getUpdateExceptionListItemSchemaMock } from '../../common/schemas/request/update_exception_list_item_schema.mock';
import { getUpdateExceptionListSchemaMock } from '../../common/schemas/request/update_exception_list_schema.mock';
import {
CreateExceptionListItemSchema,
CreateExceptionListSchema,
ExceptionListItemSchema,
ExceptionListSchema,
} from '../../common/schemas';
import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../common/schemas';
import { getFoundExceptionListSchemaMock } from '../../common/schemas/response/found_exception_list_schema.mock';

import {
Expand All @@ -25,14 +20,20 @@ import {
addExceptionListItem,
deleteExceptionListById,
deleteExceptionListItemById,
exportExceptionList,
fetchExceptionListById,
fetchExceptionListItemById,
fetchExceptionLists,
fetchExceptionListsItemsByListIds,
updateExceptionList,
updateExceptionListItem,
} from './api';
import { ApiCallByIdProps, ApiCallByListIdProps, ApiCallFetchExceptionListsProps } from './types';
import {
ApiCallByIdProps,
ApiCallByListIdProps,
ApiCallFetchExceptionListsProps,
ExportExceptionListProps,
} from './types';

const abortCtrl = new AbortController();

Expand Down Expand Up @@ -73,36 +74,6 @@ describe('Exceptions Lists API', () => {
});
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
});

test('it returns error and does not make request if request payload fails decode', async () => {
const payload: Omit<CreateExceptionListSchema, 'description'> & {
description?: string[];
} = { ...getCreateExceptionListSchemaMock(), description: ['123'] };

await expect(
addExceptionList({
http: httpMock,
list: (payload as unknown) as ExceptionListSchema,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "["123"]" supplied to "description"');
});

test('it returns error if response payload fails decode', async () => {
const payload = getCreateExceptionListSchemaMock();
const badPayload = getExceptionListSchemaMock();
// @ts-expect-error
delete badPayload.id;
httpMock.fetch.mockResolvedValue(badPayload);

await expect(
addExceptionList({
http: httpMock,
list: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});

describe('#addExceptionListItem', () => {
Expand Down Expand Up @@ -870,4 +841,66 @@ 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);
});

test('it returns error and does not make request if request payload fails decode', async () => {
const badRequest = {
http: httpMock,
id: 'some-id',
listId: 'list-id',
namespaceType: 'single',
signal: abortCtrl.signal,
};
// @ts-expect-error
delete badRequest.id;

await expect(exportExceptionList(badRequest as ExportExceptionListProps)).rejects.toEqual(
'Invalid value "undefined" supplied to "id"'
);
});
});
});
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

* @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