[SIEM][Exceptions] - Updates exception structure and corresponding UI types (#69120) (#69540)

### Summary

This PR is meant to update the `ExceptionListItemSchema.entries` structure to align with the most recent conversations regarding the need for a more explicit depiction of `nested` fields. To summarize:

- Adds schema validation for requests and responses within `lists/public/exceptions/api.ts`. It was super helpful in catching existing bugs. Anyone that uses the api will run through this validation. If the client tries to send up a malformed request, the request will not be made and an error returned. If the request is successful, but somehow the response is malformed, an error is returned. There may be some UX things to figure out about how to best communicate these errors to the user, or if surfacing the raw error is fine.
- Updates `entries` structure in lists plugin api
- Updates hooks and tests within `lists/public` that make reference to new structure
- Updates and adds unit tests for updated schemas
- Removes unused temporary types in `security_solution/public/common/components/exceptions/` to now reference updated schema
- Updates UI tests
- Updates `lists/server/scripts`
This commit is contained in:
Yara Tercero 2020-06-18 14:58:16 -04:00 committed by GitHub
parent c89d1f0677
commit ea58554daa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 3066 additions and 835 deletions

View file

@ -3,6 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EntriesArray } from './schemas/types';
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
export const USER = 'some user';
export const LIST_INDEX = '.lists';
@ -32,9 +34,25 @@ export const NAMESPACE_TYPE = 'single';
// Exception List specific
export const ID = 'uuid_here';
export const ITEM_ID = 'some-list-item-id';
export const ENDPOINT_TYPE = 'endpoint';
export const ENTRIES = [
{ field: 'some.field', match: 'some value', match_any: undefined, operator: 'included' },
export const FIELD = 'host.name';
export const OPERATOR = 'included';
export const ENTRY_VALUE = 'some host name';
export const MATCH = 'match';
export const MATCH_ANY = 'match_any';
export const LIST = 'list';
export const EXISTS = 'exists';
export const NESTED = 'nested';
export const ENTRIES: EntriesArray = [
{
entries: [
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
],
field: 'some.field',
type: 'nested',
},
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
];
export const ITEM_TYPE = 'simple';
export const _TAGS = [];

View file

@ -0,0 +1,63 @@
/*
* 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 { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../siem_common_deps';
import { operator_type as operatorType } from './schemas';
describe('Common schemas', () => {
describe('operatorType', () => {
test('it should validate for "match"', () => {
const payload = 'match';
const decoded = operatorType.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate for "match_any"', () => {
const payload = 'match_any';
const decoded = operatorType.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate for "list"', () => {
const payload = 'list';
const decoded = operatorType.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate for "exists"', () => {
const payload = 'exists';
const decoded = operatorType.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should contain 4 keys', () => {
// Might seem like a weird test, but its meant to
// ensure that if operatorType is updated, you
// also update the OperatorTypeEnum, a workaround
// for io-ts not yet supporting enums
// https://github.com/gcanti/io-ts/issues/67
const keys = Object.keys(operatorType.keys);
expect(keys.length).toEqual(4);
});
});
});

View file

@ -127,3 +127,21 @@ export type CursorOrUndefined = t.TypeOf<typeof cursorOrUndefined>;
export const namespace_type = DefaultNamespace;
export type NamespaceType = t.TypeOf<typeof namespace_type>;
export const operator = t.keyof({ excluded: null, included: null });
export type Operator = t.TypeOf<typeof operator>;
export const operator_type = t.keyof({
exists: null,
list: null,
match: null,
match_any: null,
});
export type OperatorType = t.TypeOf<typeof operator_type>;
export enum OperatorTypeEnum {
NESTED = 'nested',
MATCH = 'match',
MATCH_ANY = 'match_any',
EXISTS = 'exists',
LIST = 'list',
}

View file

@ -15,16 +15,15 @@ import {
} from './create_exception_list_item_schema';
import { getCreateExceptionListItemSchemaMock } from './create_exception_list_item_schema.mock';
describe('create_exception_list_schema', () => {
test('it should validate a typical exception list item request', () => {
describe('create_exception_list_item_schema', () => {
test('it should validate a typical exception list item request not counting the auto generated uuid', () => {
const payload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
const decoded = createExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
expect(message.schema).toEqual(payload);
});
test('it should not accept an undefined for "description"', () => {
@ -75,20 +74,20 @@ describe('create_exception_list_schema', () => {
expect(message.schema).toEqual({});
});
test('it should accept an undefined for "meta" but strip it out', () => {
test('it should accept an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
const payload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete payload.meta;
delete outputPayload.meta;
const decoded = createExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
delete outputPayload.meta;
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "comments" but return an array', () => {
test('it should accept an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.comments;
@ -96,7 +95,7 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
@ -109,12 +108,12 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "namespace_type" but return enum "single"', () => {
test('it should accept an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.namespace_type;
@ -122,12 +121,12 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "tags" but return an array', () => {
test('it should accept an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.tags;
@ -135,12 +134,12 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "_tags" but return an array', () => {
test('it should accept an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload._tags;
@ -148,7 +147,7 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id;
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});

View file

@ -4,17 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DESCRIPTION, LIST_ID, META, NAME, NAMESPACE_TYPE, TYPE } from '../../constants.mock';
import { DESCRIPTION, ENDPOINT_TYPE, META, NAME, NAMESPACE_TYPE } from '../../constants.mock';
import { CreateExceptionListSchema } from './create_exception_list_schema';
export const getCreateExceptionListSchemaMock = (): CreateExceptionListSchema => ({
_tags: [],
description: DESCRIPTION,
list_id: LIST_ID,
list_id: undefined,
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
tags: [],
type: TYPE,
type: ENDPOINT_TYPE,
});

View file

@ -16,27 +16,28 @@ import {
import { getCreateExceptionListSchemaMock } from './create_exception_list_schema.mock';
describe('create_exception_list_schema', () => {
test('it should validate a typical exception lists request', () => {
test('it should validate a typical exception lists request and generate a correct body not counting the uuid', () => {
const payload = getCreateExceptionListSchemaMock();
const decoded = createExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListSchema).list_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should accept an undefined for meta', () => {
test('it should accept an undefined for "meta" and generate a correct body not counting the uuid', () => {
const payload = getCreateExceptionListSchemaMock();
delete payload.meta;
const decoded = createExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListSchema).list_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should accept an undefined for tags but return an array', () => {
test('it should accept an undefined for "tags" but return an array and generate a correct body not counting the uuid', () => {
const inputPayload = getCreateExceptionListSchemaMock();
const outputPayload = getCreateExceptionListSchemaMock();
delete inputPayload.tags;
@ -44,11 +45,12 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListSchema).list_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for _tags but return an array', () => {
test('it should accept an undefined for "_tags" but return an array and generate a correct body not counting the uuid', () => {
const inputPayload = getCreateExceptionListSchemaMock();
const outputPayload = getCreateExceptionListSchemaMock();
delete inputPayload._tags;
@ -56,11 +58,12 @@ describe('create_exception_list_schema', () => {
const decoded = createExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListSchema).list_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for list_id and auto generate a uuid', () => {
test('it should accept an undefined for "list_id" and auto generate a uuid', () => {
const inputPayload = getCreateExceptionListSchemaMock();
delete inputPayload.list_id;
const decoded = createExceptionListSchema.decode(inputPayload);
@ -72,7 +75,7 @@ describe('create_exception_list_schema', () => {
);
});
test('it should accept an undefined for list_id and generate a correct body not counting the uuid', () => {
test('it should accept an undefined for "list_id" and generate a correct body not counting the uuid', () => {
const inputPayload = getCreateExceptionListSchemaMock();
delete inputPayload.list_id;
const decoded = createExceptionListSchema.decode(inputPayload);

View file

@ -0,0 +1,14 @@
/*
* 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, NAMESPACE_TYPE } from '../../constants.mock';
import { DeleteExceptionListItemSchema } from './delete_exception_list_item_schema';
export const getDeleteExceptionListItemSchemaMock = (): DeleteExceptionListItemSchema => ({
id: ID,
namespace_type: NAMESPACE_TYPE,
});

View file

@ -0,0 +1,61 @@
/*
* 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 '../../siem_common_deps';
import {
DeleteExceptionListItemSchema,
deleteExceptionListItemSchema,
} from './delete_exception_list_item_schema';
import { getDeleteExceptionListItemSchemaMock } from './delete_exception_list_item_schema.mock';
describe('delete_exception_list_item_schema', () => {
test('it should validate a typical exception list item request', () => {
const payload = getDeleteExceptionListItemSchemaMock();
const decoded = deleteExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
// TODO It does allow an id of undefined, is this wanted behavior?
test.skip('it should NOT accept an undefined for an "id"', () => {
const payload = getDeleteExceptionListItemSchemaMock();
delete payload.id;
const decoded = deleteExceptionListItemSchema.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 accept an undefined for "namespace_type" but default to "single"', () => {
const payload = getDeleteExceptionListItemSchemaMock();
delete payload.namespace_type;
const decoded = deleteExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getDeleteExceptionListItemSchemaMock());
});
test('it should not allow an extra key to be sent in', () => {
const payload: DeleteExceptionListItemSchema & {
extraKey?: string;
} = getDeleteExceptionListItemSchemaMock();
payload.extraKey = 'some new value';
const decoded = deleteExceptionListItemSchema.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({});
});
});

View file

@ -0,0 +1,14 @@
/*
* 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, NAMESPACE_TYPE } from '../../constants.mock';
import { DeleteExceptionListSchema } from './delete_exception_list_schema';
export const getDeleteExceptionListSchemaMock = (): DeleteExceptionListSchema => ({
id: ID,
namespace_type: NAMESPACE_TYPE,
});

View file

@ -0,0 +1,61 @@
/*
* 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 '../../siem_common_deps';
import {
DeleteExceptionListSchema,
deleteExceptionListSchema,
} from './delete_exception_list_schema';
import { getDeleteExceptionListSchemaMock } from './delete_exception_list_schema.mock';
describe('delete_exception_list_schema', () => {
test('it should validate a typical exception list request', () => {
const payload = getDeleteExceptionListSchemaMock();
const decoded = deleteExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
// TODO It does allow an id of undefined, is this wanted behavior?
test.skip('it should NOT accept an undefined for an id', () => {
const payload = getDeleteExceptionListSchemaMock();
delete payload.id;
const decoded = deleteExceptionListSchema.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 accept an undefined for "namespace_type" but default to "single"', () => {
const payload = getDeleteExceptionListSchemaMock();
delete payload.namespace_type;
const decoded = deleteExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getDeleteExceptionListSchemaMock());
});
test('it should not allow an extra key to be sent in', () => {
const payload: DeleteExceptionListSchema & {
extraKey?: string;
} = getDeleteExceptionListSchemaMock();
payload.extraKey = 'some new value';
const decoded = deleteExceptionListSchema.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({});
});
});

View file

@ -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, ITEM_ID, NAMESPACE_TYPE } from '../../constants.mock';
import { ReadExceptionListItemSchema } from './read_exception_list_item_schema';
export const getReadExceptionListItemSchemaMock = (): ReadExceptionListItemSchema => ({
id: ID,
item_id: ITEM_ID,
namespace_type: NAMESPACE_TYPE,
});

View file

@ -0,0 +1,129 @@
/*
* 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 '../../siem_common_deps';
import { getReadExceptionListItemSchemaMock } from './read_exception_list_item_schema.mock';
import {
ReadExceptionListItemSchema,
readExceptionListItemSchema,
} from './read_exception_list_item_schema';
describe('read_exception_list_item_schema', () => {
test('it should validate a typical exception list request', () => {
const payload = getReadExceptionListItemSchemaMock();
const decoded = readExceptionListItemSchema.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 accept an undefined for "id"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.id;
const decoded = readExceptionListItemSchema.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 accept an undefined for "item_id"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.item_id;
const decoded = readExceptionListItemSchema.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 accept an undefined for "namespace_type" but default to "single"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.namespace_type;
const decoded = readExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getReadExceptionListItemSchemaMock());
});
test('it should accept an undefined for "id", "item_id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.id;
delete payload.namespace_type;
delete payload.item_id;
const output = getReadExceptionListItemSchemaMock();
delete output.id;
delete output.item_id;
const decoded = readExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should accept an undefined for "id", "item_id"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.id;
delete payload.item_id;
const decoded = readExceptionListItemSchema.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 accept an undefined for "id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.id;
delete payload.namespace_type;
const output = getReadExceptionListItemSchemaMock();
delete output.id;
const decoded = readExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should accept an undefined for "item_id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getReadExceptionListItemSchemaMock();
delete payload.namespace_type;
delete payload.item_id;
const output = getReadExceptionListItemSchemaMock();
delete output.item_id;
const decoded = readExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should not allow an extra key to be sent in', () => {
const payload: ReadExceptionListItemSchema & {
extraKey?: string;
} = getReadExceptionListItemSchemaMock();
payload.extraKey = 'some new value';
const decoded = readExceptionListItemSchema.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({});
});
});

View file

@ -11,11 +11,13 @@ import * as t from 'io-ts';
import { NamespaceType, id, item_id, namespace_type } from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
export const readExceptionListItemSchema = t.partial({
id,
item_id,
namespace_type, // defaults to 'single' if not set during decode
});
export const readExceptionListItemSchema = t.exact(
t.partial({
id,
item_id,
namespace_type, // defaults to 'single' if not set during decode
})
);
export type ReadExceptionListItemSchemaPartial = t.TypeOf<typeof readExceptionListItemSchema>;

View file

@ -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 { ReadExceptionListSchema } from './read_exception_list_schema';
export const getReadExceptionListSchemaMock = (): ReadExceptionListSchema => ({
id: ID,
list_id: LIST_ID,
namespace_type: NAMESPACE_TYPE,
});

View file

@ -0,0 +1,126 @@
/*
* 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 '../../siem_common_deps';
import { getReadExceptionListSchemaMock } from './read_exception_list_schema.mock';
import { ReadExceptionListSchema, readExceptionListSchema } from './read_exception_list_schema';
describe('read_exception_list_schema', () => {
test('it should validate a typical exception list request', () => {
const payload = getReadExceptionListSchemaMock();
const decoded = readExceptionListSchema.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 accept an undefined for "id"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.id;
const decoded = readExceptionListSchema.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 accept an undefined for "list_id"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.list_id;
const decoded = readExceptionListSchema.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 accept an undefined for "namespace_type" but default to "single"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.namespace_type;
const decoded = readExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getReadExceptionListSchemaMock());
});
test('it should accept an undefined for "id", "list_id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.id;
delete payload.namespace_type;
delete payload.list_id;
const output = getReadExceptionListSchemaMock();
delete output.id;
delete output.list_id;
const decoded = readExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should accept an undefined for "id", "list_id"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.id;
delete payload.list_id;
const decoded = readExceptionListSchema.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 accept an undefined for "id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.id;
delete payload.namespace_type;
const output = getReadExceptionListSchemaMock();
delete output.id;
const decoded = readExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should accept an undefined for "list_id", "namespace_type" but default "namespace_type" to "single"', () => {
const payload = getReadExceptionListSchemaMock();
delete payload.namespace_type;
delete payload.list_id;
const output = getReadExceptionListSchemaMock();
delete output.list_id;
const decoded = readExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(output);
});
test('it should not allow an extra key to be sent in', () => {
const payload: ReadExceptionListSchema & {
extraKey?: string;
} = getReadExceptionListSchemaMock();
payload.extraKey = 'some new value';
const decoded = readExceptionListSchema.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({});
});
});

View file

@ -11,11 +11,13 @@ import * as t from 'io-ts';
import { NamespaceType, id, list_id, namespace_type } from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
export const readExceptionListSchema = t.partial({
id,
list_id,
namespace_type, // defaults to 'single' if not set during decode
});
export const readExceptionListSchema = t.exact(
t.partial({
id,
list_id,
namespace_type, // defaults to 'single' if not set during decode
})
);
export type ReadExceptionListSchemaPartial = t.TypeOf<typeof readExceptionListSchema>;

View file

@ -0,0 +1,21 @@
/*
* 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 { DESCRIPTION, ID, LIST_ID, META, NAME, NAMESPACE_TYPE, _TAGS } from '../../constants.mock';
import { UpdateExceptionListSchema } from './update_exception_list_schema';
export const getUpdateExceptionListSchemaMock = (): UpdateExceptionListSchema => ({
_tags: _TAGS,
description: DESCRIPTION,
id: ID,
list_id: LIST_ID,
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
tags: ['malware'],
type: 'endpoint',
});

View file

@ -0,0 +1,147 @@
/*
* 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 '../../siem_common_deps';
import {
UpdateExceptionListSchema,
updateExceptionListSchema,
} from './update_exception_list_schema';
import { getUpdateExceptionListSchemaMock } from './update_exception_list_schema.mock';
describe('update_exception_list_schema', () => {
test('it should validate a typical exception list request', () => {
const payload = getUpdateExceptionListSchemaMock();
const decoded = updateExceptionListSchema.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 "description"', () => {
const payload = getUpdateExceptionListSchemaMock();
delete payload.description;
const decoded = updateExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "description"',
]);
expect(message.schema).toEqual({});
});
test('it should not accept an undefined for "name"', () => {
const payload = getUpdateExceptionListSchemaMock();
delete payload.name;
const decoded = updateExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "name"',
]);
expect(message.schema).toEqual({});
});
test('it should not accept an undefined for "type"', () => {
const payload = getUpdateExceptionListSchemaMock();
delete payload.type;
const decoded = updateExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should accept an undefined for "meta" but strip it out', () => {
const payload = getUpdateExceptionListSchemaMock();
const outputPayload = getUpdateExceptionListSchemaMock();
delete payload.meta;
const decoded = updateExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
delete outputPayload.meta;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "namespace_type" but return enum "single"', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
const outputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload.namespace_type;
outputPayload.namespace_type = 'single';
const decoded = updateExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "tags" but return an array', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
const outputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload.tags;
outputPayload.tags = [];
const decoded = updateExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "_tags" but return an array', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
const outputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
const decoded = updateExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
// TODO: Is it expected behavior for it not to auto-generate a uui or throw
// error if list_id is not passed in?
test.skip('it should accept an undefined for "list_id" and auto generate a uuid', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload.list_id;
const decoded = updateExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as UpdateExceptionListSchema).list_id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
});
test('it should accept an undefined for "list_id" and generate a correct body not counting the uuid', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload.list_id;
const decoded = updateExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as UpdateExceptionListSchema).list_id;
expect(message.schema).toEqual(inputPayload);
});
test('it should not allow an extra key to be sent in', () => {
const payload: UpdateExceptionListSchema & {
extraKey?: string;
} = getUpdateExceptionListSchemaMock();
payload.extraKey = 'some new value';
const decoded = updateExceptionListSchema.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({});
});
});

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ENTRIES } from '../../constants.mock';
import { ExceptionListItemSchema } from './exception_list_item_schema';
@ -12,20 +13,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
description: 'This is a sample endpoint type exception',
entries: [
{
field: 'actingProcess.file.signer',
match: 'Elastic, N.V.',
match_any: undefined,
operator: 'included',
},
{
field: 'event.category',
match: undefined,
match_any: ['process', 'malware'],
operator: 'included',
},
],
entries: ENTRIES,
id: '1',
item_id: 'endpoint_list_item',
list_id: 'endpoint_list',

View file

@ -0,0 +1,191 @@
/*
* 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 '../../siem_common_deps';
import { getExceptionListSchemaMock } from './exception_list_schema.mock';
import { ExceptionListSchema, exceptionListSchema } from './exception_list_schema';
describe('exception_list_schema', () => {
test('it should validate a typical exception list response', () => {
const payload = getExceptionListSchemaMock();
const decoded = exceptionListSchema.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 "id"', () => {
const payload = getExceptionListSchemaMock();
delete payload.id;
const decoded = exceptionListSchema.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 NOT accept an undefined for "list_id"', () => {
const payload = getExceptionListSchemaMock();
delete payload.list_id;
const decoded = exceptionListSchema.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 accept an undefined for "name"', () => {
const payload = getExceptionListSchemaMock();
delete payload.name;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "name"',
]);
expect(message.schema).toEqual({});
});
// TODO: Should this throw an error? "namespace_type" gets auto-populated
// with default "single", is that desired behavior?
test.skip('it should NOT accept an undefined for "namespace_type"', () => {
const payload = getExceptionListSchemaMock();
delete payload.namespace_type;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "namespace_type"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "description"', () => {
const payload = getExceptionListSchemaMock();
delete payload.description;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "description"',
]);
expect(message.schema).toEqual({});
});
test('it should accept an undefined for "meta"', () => {
const payload = getExceptionListSchemaMock();
delete payload.meta;
const decoded = exceptionListSchema.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 "created_at"', () => {
const payload = getExceptionListSchemaMock();
delete payload.created_at;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "created_at"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "created_by"', () => {
const payload = getExceptionListSchemaMock();
delete payload.created_by;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "created_by"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "tie_breaker_id"', () => {
const payload = getExceptionListSchemaMock();
delete payload.tie_breaker_id;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "tie_breaker_id"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "type"', () => {
const payload = getExceptionListSchemaMock();
delete payload.type;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "updated_at"', () => {
const payload = getExceptionListSchemaMock();
delete payload.updated_at;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "updated_at"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "updated_by"', () => {
const payload = getExceptionListSchemaMock();
delete payload.updated_by;
const decoded = exceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "updated_by"',
]);
expect(message.schema).toEqual({});
});
test('it should not allow an extra key to be sent in', () => {
const payload: ExceptionListSchema & {
extraKey?: string;
} = getExceptionListSchemaMock();
payload.extraKey = 'some new value';
const decoded = exceptionListSchema.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({});
});
});

View file

@ -0,0 +1,151 @@
/*
* 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 '../../siem_common_deps';
import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock';
import { getFoundExceptionListItemSchemaMock } from './found_exception_list_item_schema.mock';
import {
FoundExceptionListItemSchema,
foundExceptionListItemSchema,
} from './found_exception_list_item_schema';
import { ExceptionListItemSchema } from './exception_list_item_schema';
describe('found_exception_list_item_schema', () => {
test('it should validate a typical exception list response', () => {
const payload = getFoundExceptionListItemSchemaMock();
const decoded = foundExceptionListItemSchema.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 a malformed exception list item within "data"', () => {
const item: Omit<ExceptionListItemSchema, 'entries'> & {
entries?: string;
} = { ...getExceptionListItemSchemaMock(), entries: 'I should be an array' };
const payload: Omit<FoundExceptionListItemSchema, 'data'> & {
data?: Array<
Omit<ExceptionListItemSchema, 'entries'> & {
entries?: string;
}
>;
} = { ...getFoundExceptionListItemSchemaMock(), data: [item] };
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "I should be an array" supplied to "data,entries"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept a string for "page"', () => {
const payload: Omit<FoundExceptionListItemSchema, 'page'> & {
page?: string;
} = { ...getFoundExceptionListItemSchemaMock(), page: '1' };
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "page"']);
expect(message.schema).toEqual({});
});
test('it should NOT accept a string for "per_page"', () => {
const payload: Omit<FoundExceptionListItemSchema, 'per_page'> & {
per_page?: string;
} = { ...getFoundExceptionListItemSchemaMock(), per_page: '20' };
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "20" supplied to "per_page"']);
expect(message.schema).toEqual({});
});
test('it should NOT accept a string for "total"', () => {
const payload: Omit<FoundExceptionListItemSchema, 'total'> & {
total?: string;
} = { ...getFoundExceptionListItemSchemaMock(), total: '1' };
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "total"']);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "page"', () => {
const payload = getFoundExceptionListItemSchemaMock();
delete payload.page;
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "page"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "per_page"', () => {
const payload = getFoundExceptionListItemSchemaMock();
delete payload.per_page;
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "per_page"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "total"', () => {
const payload = getFoundExceptionListItemSchemaMock();
delete payload.total;
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "total"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "data"', () => {
const payload = getFoundExceptionListItemSchemaMock();
delete payload.data;
const decoded = foundExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "data"',
]);
expect(message.schema).toEqual({});
});
test('it should not allow an extra key to be sent in', () => {
const payload: FoundExceptionListItemSchema & {
extraKey?: string;
} = getFoundExceptionListItemSchemaMock();
payload.extraKey = 'some new value';
const decoded = foundExceptionListItemSchema.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({});
});
});

View file

@ -0,0 +1,148 @@
/*
* 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 '../../siem_common_deps';
import { getExceptionListSchemaMock } from './exception_list_schema.mock';
import { getFoundExceptionListSchemaMock } from './found_exception_list_schema.mock';
import { FoundExceptionListSchema, foundExceptionListSchema } from './found_exception_list_schema';
import { ExceptionListSchema } from './exception_list_schema';
describe('exception_list_schema', () => {
test('it should validate a typical exception list response', () => {
const payload = getFoundExceptionListSchemaMock();
const decoded = foundExceptionListSchema.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 a malformed exception list item within "data"', () => {
const item: Omit<ExceptionListSchema, 'entries'> & {
entries?: string[];
} = { ...getExceptionListSchemaMock(), entries: ['I should not be here'] };
const payload: Omit<FoundExceptionListSchema, 'data'> & {
data?: Array<
Omit<ExceptionListSchema, 'entries'> & {
entries?: string[];
}
>;
} = { ...getFoundExceptionListSchemaMock(), data: [item] };
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'invalid keys "entries,["I should not be here"]"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept a string for "page"', () => {
const payload: Omit<FoundExceptionListSchema, 'page'> & {
page?: string;
} = { ...getFoundExceptionListSchemaMock(), page: '1' };
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "page"']);
expect(message.schema).toEqual({});
});
test('it should NOT accept a string for "per_page"', () => {
const payload: Omit<FoundExceptionListSchema, 'per_page'> & {
per_page?: string;
} = { ...getFoundExceptionListSchemaMock(), per_page: '20' };
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "20" supplied to "per_page"']);
expect(message.schema).toEqual({});
});
test('it should NOT accept a string for "total"', () => {
const payload: Omit<FoundExceptionListSchema, 'total'> & {
total?: string;
} = { ...getFoundExceptionListSchemaMock(), total: '1' };
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "total"']);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "page"', () => {
const payload = getFoundExceptionListSchemaMock();
delete payload.page;
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "page"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "per_page"', () => {
const payload = getFoundExceptionListSchemaMock();
delete payload.per_page;
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "per_page"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "total"', () => {
const payload = getFoundExceptionListSchemaMock();
delete payload.total;
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "total"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT accept an undefined for "data"', () => {
const payload = getFoundExceptionListSchemaMock();
delete payload.data;
const decoded = foundExceptionListSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "data"',
]);
expect(message.schema).toEqual({});
});
test('it should not allow an extra key to be sent in', () => {
const payload: FoundExceptionListSchema & {
extraKey?: string;
} = getFoundExceptionListSchemaMock();
payload.extraKey = 'some new value';
const decoded = foundExceptionListSchema.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({});
});
});

View file

@ -0,0 +1,22 @@
/*
* 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 { DATE_NOW, USER } from '../../constants.mock';
import { CommentsArray } from './comments';
export const getCommentsMock = (): CommentsArray => [
{
comment: 'some comment',
created_at: DATE_NOW,
created_by: USER,
},
{
comment: 'some other comment',
created_at: DATE_NOW,
created_by: 'lily',
},
];

View file

@ -0,0 +1,98 @@
/*
* 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 { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../siem_common_deps';
import { DefaultEntryArray } from './default_entries_array';
import { EntriesArray } from './entries';
import { getEntriesArrayMock, getEntryMatchMock, getEntryNestedMock } from './entries.mock';
// NOTE: This may seem weird, but when validating schemas that use a union
// it checks against every item in that union. Since entries consist of 5
// different entry types, it returns 5 of these. To make more readable,
// extracted here.
const returnedSchemaError = `"Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "list", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "exists" |} | {| entries: Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "list", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "exists" |})>, field: string, type: "nested" |})>"`;
describe('default_entries_array', () => {
test('it should validate an empty array', () => {
const payload: EntriesArray = [];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of regular and nested entries', () => {
const payload: EntriesArray = getEntriesArrayMock();
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of nested entries', () => {
const payload: EntriesArray = [{ ...getEntryNestedMock() }];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of non nested entries', () => {
const payload: EntriesArray = [{ ...getEntryMatchMock() }];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate an array of numbers', () => {
const payload = [1];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
// TODO: Known weird error formatting that is on our list to address
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an array of strings', () => {
const payload = ['some string'];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -7,7 +7,7 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { EntriesArray, entries } from './entries';
import { EntriesArray, entriesArray } from './entries';
export type DefaultEntriesArrayC = t.Type<EntriesArray, EntriesArray, unknown>;
@ -21,8 +21,8 @@ export const DefaultEntryArray: DefaultEntriesArrayC = new t.Type<
unknown
>(
'DefaultEntryArray',
t.array(entries).is,
(input, context): Either<t.Errors, EntriesArray> =>
input == null ? t.success([]) : t.array(entries).validate(input, context),
entriesArray.is,
(input): Either<t.Errors, EntriesArray> =>
input == null ? t.success([]) : entriesArray.decode(input),
t.identity
);

View file

@ -0,0 +1,66 @@
/*
* 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 {
ENTRY_VALUE,
EXISTS,
FIELD,
LIST,
MATCH,
MATCH_ANY,
NESTED,
OPERATOR,
} from '../../constants.mock';
import {
EntriesArray,
EntryExists,
EntryList,
EntryMatch,
EntryMatchAny,
EntryNested,
} from './entries';
export const getEntryMatchMock = (): EntryMatch => ({
field: FIELD,
operator: OPERATOR,
type: MATCH,
value: ENTRY_VALUE,
});
export const getEntryMatchAnyMock = (): EntryMatchAny => ({
field: FIELD,
operator: OPERATOR,
type: MATCH_ANY,
value: [ENTRY_VALUE],
});
export const getEntryListMock = (): EntryList => ({
field: FIELD,
operator: OPERATOR,
type: LIST,
value: [ENTRY_VALUE],
});
export const getEntryExistsMock = (): EntryExists => ({
field: FIELD,
operator: OPERATOR,
type: EXISTS,
});
export const getEntryNestedMock = (): EntryNested => ({
entries: [getEntryMatchMock(), getEntryExistsMock()],
field: FIELD,
type: NESTED,
});
export const getEntriesArrayMock = (): EntriesArray => [
getEntryMatchMock(),
getEntryMatchAnyMock(),
getEntryListMock(),
getEntryExistsMock(),
getEntryNestedMock(),
];

View file

@ -0,0 +1,353 @@
/*
* 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 { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../siem_common_deps';
import {
getEntryExistsMock,
getEntryListMock,
getEntryMatchAnyMock,
getEntryMatchMock,
getEntryNestedMock,
} from './entries.mock';
import {
EntryExists,
EntryList,
EntryMatch,
EntryMatchAny,
EntryNested,
entriesExists,
entriesList,
entriesMatch,
entriesMatchAny,
entriesNested,
} from './entries';
describe('Entries', () => {
describe('entriesMatch', () => {
test('it should validate an entry', () => {
const payload = getEntryMatchMock();
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "included"', () => {
const payload = getEntryMatchMock();
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "excluded"', () => {
const payload = getEntryMatchMock();
payload.operator = 'excluded';
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when "value" is not string', () => {
const payload: Omit<EntryMatch, 'value'> & { value: string[] } = {
...getEntryMatchMock(),
value: ['some value'],
};
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "["some value"]" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate when "type" is not "match"', () => {
const payload: Omit<EntryMatch, 'type'> & { type: string } = {
...getEntryMatchMock(),
type: 'match_any',
};
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "match_any" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryMatch & {
extraKey?: string;
} = getEntryMatchMock();
payload.extraKey = 'some value';
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryMatchMock());
});
});
describe('entriesMatchAny', () => {
test('it should validate an entry', () => {
const payload = getEntryMatchAnyMock();
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "included"', () => {
const payload = getEntryMatchAnyMock();
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "excluded"', () => {
const payload = getEntryMatchAnyMock();
payload.operator = 'excluded';
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when value is not string array', () => {
const payload: Omit<EntryMatchAny, 'value'> & { value: string } = {
...getEntryMatchAnyMock(),
value: 'some string',
};
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate when "type" is not "match_any"', () => {
const payload: Omit<EntryMatchAny, 'type'> & { type: string } = {
...getEntryMatchAnyMock(),
type: 'match',
};
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryMatchAny & {
extraKey?: string;
} = getEntryMatchAnyMock();
payload.extraKey = 'some extra key';
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryMatchAnyMock());
});
});
describe('entriesExists', () => {
test('it should validate an entry', () => {
const payload = getEntryExistsMock();
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "included"', () => {
const payload = getEntryExistsMock();
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "excluded"', () => {
const payload = getEntryExistsMock();
payload.operator = 'excluded';
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should strip out extra keys', () => {
const payload: EntryExists & {
extraKey?: string;
} = getEntryExistsMock();
payload.extraKey = 'some extra key';
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryExistsMock());
});
test('it should not validate when "type" is not "exists"', () => {
const payload: Omit<EntryExists, 'type'> & { type: string } = {
...getEntryExistsMock(),
type: 'match',
};
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
});
describe('entriesList', () => {
test('it should validate an entry', () => {
const payload = getEntryListMock();
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "included"', () => {
const payload = getEntryListMock();
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "excluded"', () => {
const payload = getEntryListMock();
payload.operator = 'excluded';
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when "value" is not string array', () => {
const payload: Omit<EntryList, 'value'> & { value: string } = {
...getEntryListMock(),
value: 'someListId',
};
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "someListId" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate when "type" is not "lists"', () => {
const payload: Omit<EntryList, 'type'> & { type: 'match_any' } = {
...getEntryListMock(),
type: 'match_any',
};
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "match_any" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryList & {
extraKey?: string;
} = getEntryListMock();
payload.extraKey = 'some extra key';
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryListMock());
});
});
describe('entriesNested', () => {
test('it should validate a nested entry', () => {
const payload = getEntryNestedMock();
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate when "type" is not "nested"', () => {
const payload: Omit<EntryNested, 'type'> & { type: 'match' } = {
...getEntryNestedMock(),
type: 'match',
};
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
test('it should NOT validate when "field" is not a string', () => {
const payload: Omit<EntryNested, 'field'> & {
field: number;
} = { ...getEntryNestedMock(), field: 1 };
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']);
expect(message.schema).toEqual({});
});
test('it should NOT validate when "entries" is not a an array', () => {
const payload: Omit<EntryNested, 'entries'> & {
entries: string;
} = { ...getEntryNestedMock(), entries: 'im a string' };
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "im a string" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryNested & {
extraKey?: string;
} = getEntryNestedMock();
payload.extraKey = 'some extra key';
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryNestedMock());
});
});
});

View file

@ -3,19 +3,67 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import * as t from 'io-ts';
export const entries = t.exact(
import { operator } from '../common/schemas';
import { DefaultStringArray } from '../../siem_common_deps';
export const entriesMatch = t.exact(
t.type({
field: t.string,
match: t.union([t.string, t.undefined]),
match_any: t.union([t.array(t.string), t.undefined]),
operator: t.string, // TODO: Use a key of with all possible values
operator,
type: t.keyof({ match: null }),
value: t.string,
})
);
export type EntryMatch = t.TypeOf<typeof entriesMatch>;
export const entriesArray = t.array(entries);
export const entriesMatchAny = t.exact(
t.type({
field: t.string,
operator,
type: t.keyof({ match_any: null }),
value: DefaultStringArray,
})
);
export type EntryMatchAny = t.TypeOf<typeof entriesMatchAny>;
export const entriesList = t.exact(
t.type({
field: t.string,
operator,
type: t.keyof({ list: null }),
value: DefaultStringArray,
})
);
export type EntryList = t.TypeOf<typeof entriesList>;
export const entriesExists = t.exact(
t.type({
field: t.string,
operator,
type: t.keyof({ exists: null }),
})
);
export type EntryExists = t.TypeOf<typeof entriesExists>;
export const entriesNested = t.exact(
t.type({
entries: t.array(t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists])),
field: t.string,
type: t.keyof({ nested: null }),
})
);
export type EntryNested = t.TypeOf<typeof entriesNested>;
export const entry = t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists]);
export type Entry = t.TypeOf<typeof entry>;
export const entriesArray = t.array(
t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists, entriesNested])
);
export type EntriesArray = t.TypeOf<typeof entriesArray>;
export type Entries = t.TypeOf<typeof entries>;
export const entriesArrayOrUndefined = t.union([entriesArray, t.undefined]);
export type EntriesArrayOrUndefined = t.TypeOf<typeof entriesArrayOrUndefined>;

View file

@ -1,71 +0,0 @@
/*
* 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 { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
import {
ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
} from '../../../common/schemas';
import {
AddExceptionListItemProps,
AddExceptionListProps,
ApiCallByIdProps,
ApiCallByListIdProps,
} from '../types';
/* eslint-disable @typescript-eslint/no-unused-vars */
export const addExceptionList = async ({
http,
list,
signal,
}: AddExceptionListProps): Promise<ExceptionListSchema> =>
Promise.resolve(getExceptionListSchemaMock());
export const addExceptionListItem = async ({
http,
listItem,
signal,
}: AddExceptionListItemProps): Promise<ExceptionListItemSchema> =>
Promise.resolve(getExceptionListItemSchemaMock());
export const fetchExceptionListById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> => Promise.resolve(getExceptionListSchemaMock());
export const fetchExceptionListItemsByListId = async ({
filterOptions,
http,
listId,
pagination,
signal,
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> =>
Promise.resolve({ data: [getExceptionListItemSchemaMock()], page: 1, per_page: 20, total: 1 });
export const fetchExceptionListItemById = async ({
http,
id,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
Promise.resolve(getExceptionListItemSchemaMock());
export const deleteExceptionListById = async ({
http,
id,
namespaceType,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> => Promise.resolve(getExceptionListSchemaMock());
export const deleteExceptionListItemById = async ({
http,
id,
namespaceType,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
Promise.resolve(getExceptionListItemSchemaMock());

View file

@ -8,6 +8,15 @@ import { getExceptionListSchemaMock } from '../../common/schemas/response/except
import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock';
import { getCreateExceptionListSchemaMock } from '../../common/schemas/request/create_exception_list_schema.mock';
import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/request/create_exception_list_item_schema.mock';
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 {
addExceptionList,
@ -17,7 +26,10 @@ import {
fetchExceptionListById,
fetchExceptionListItemById,
fetchExceptionListItemsByListId,
updateExceptionList,
updateExceptionListItem,
} from './api';
import { ApiCallByIdProps, ApiCallByListIdProps } from './types';
const abortCtrl = new AbortController();
@ -44,36 +56,59 @@ describe('Exceptions Lists API', () => {
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
});
test('it uses POST when "list.id" does not exist', async () => {
test('it invokes "addExceptionList" with expected url and body values', async () => {
const payload = getCreateExceptionListSchemaMock();
await addExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
});
// TODO Would like to just use getExceptionListSchemaMock() here, but
// validation returns object in different order, making the strings not match
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body: JSON.stringify(payload),
method: 'POST',
signal: abortCtrl.signal,
});
});
test('it returns expected exception list on success', async () => {
const payload = getCreateExceptionListSchemaMock();
const exceptionResponse = await addExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body: JSON.stringify(payload),
method: 'POST',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
});
test('it uses PUT when "list.id" exists', async () => {
const payload = getExceptionListSchemaMock();
const exceptionResponse = await addExceptionList({
http: mockKibanaHttpService(),
list: getExceptionListSchemaMock(),
signal: abortCtrl.signal,
});
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'] };
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body: JSON.stringify(payload),
method: 'PUT',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
await expect(
addExceptionList({
http: mockKibanaHttpService(),
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();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
addExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
@ -83,36 +118,181 @@ describe('Exceptions Lists API', () => {
fetchMock.mockResolvedValue(getExceptionListItemSchemaMock());
});
test('it uses POST when "listItem.id" does not exist', async () => {
test('it invokes "addExceptionListItem" with expected url and body values', async () => {
const payload = getCreateExceptionListItemSchemaMock();
await addExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
});
// TODO Would like to just use getExceptionListSchemaMock() here, but
// validation returns object in different order, making the strings not match
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
body: JSON.stringify(payload),
method: 'POST',
signal: abortCtrl.signal,
});
});
test('it returns expected exception list on success', async () => {
const payload = getCreateExceptionListItemSchemaMock();
const exceptionResponse = await addExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
test('it returns error and does not make request if request payload fails decode', async () => {
const payload: Omit<CreateExceptionListItemSchema, 'description'> & {
description?: string[];
} = { ...getCreateExceptionListItemSchemaMock(), description: ['123'] };
await expect(
addExceptionListItem({
http: mockKibanaHttpService(),
listItem: (payload as unknown) as ExceptionListItemSchema,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "["123"]" supplied to "description"');
});
test('it returns error if response payload fails decode', async () => {
const payload = getCreateExceptionListItemSchemaMock();
const badPayload = getExceptionListItemSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
addExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
describe('#updateExceptionList', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
});
test('it invokes "updateExceptionList" with expected url and body values', async () => {
const payload = getUpdateExceptionListSchemaMock();
await updateExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
});
// TODO Would like to just use getExceptionListSchemaMock() here, but
// validation returns object in different order, making the strings not match
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body: JSON.stringify(payload),
method: 'POST',
method: 'PUT',
signal: abortCtrl.signal,
});
});
test('it returns expected exception list on success', async () => {
const payload = getUpdateExceptionListSchemaMock();
const exceptionResponse = await updateExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
});
test('it returns error and does not make request if request payload fails decode', async () => {
const payload = getUpdateExceptionListSchemaMock();
delete payload.description;
await expect(
updateExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "description"');
});
test('it returns error if response payload fails decode', async () => {
const payload = getUpdateExceptionListSchemaMock();
const badPayload = getExceptionListSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
updateExceptionList({
http: mockKibanaHttpService(),
list: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
describe('#updateExceptionListItem', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue(getExceptionListItemSchemaMock());
});
test('it invokes "updateExceptionListItem" with expected url and body values', async () => {
const payload = getUpdateExceptionListItemSchemaMock();
await updateExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
});
// TODO Would like to just use getExceptionListSchemaMock() here, but
// validation returns object in different order, making the strings not match
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body: JSON.stringify(payload),
method: 'PUT',
signal: abortCtrl.signal,
});
});
test('it returns expected exception list on success', async () => {
const payload = getUpdateExceptionListItemSchemaMock();
const exceptionResponse = await updateExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
});
test('check parameter url, body when "listItem.id" exists', async () => {
const payload = getExceptionListItemSchemaMock();
const exceptionResponse = await addExceptionListItem({
http: mockKibanaHttpService(),
listItem: getExceptionListItemSchemaMock(),
signal: abortCtrl.signal,
});
test('it returns error and does not make request if request payload fails decode', async () => {
const payload = getUpdateExceptionListItemSchemaMock();
delete payload.description;
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
body: JSON.stringify(payload),
method: 'PUT',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
await expect(
updateExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "description"');
});
test('it returns error if response payload fails decode', async () => {
const payload = getUpdateExceptionListItemSchemaMock();
const badPayload = getExceptionListItemSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
updateExceptionListItem({
http: mockKibanaHttpService(),
listItem: payload,
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
@ -148,12 +328,39 @@ 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 = ({
http: mockKibanaHttpService(),
id: 1,
namespaceType: 'single',
signal: abortCtrl.signal,
} as unknown) as ApiCallByIdProps & { id: number };
await expect(fetchExceptionListById(payload)).rejects.toEqual(
'Invalid value "1" supplied to "id"'
);
});
test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListItemSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
fetchExceptionListById({
http: mockKibanaHttpService(),
id: '1',
namespaceType: 'single',
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
describe('#fetchExceptionListItemsByListId', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]);
fetchMock.mockResolvedValue(getFoundExceptionListItemSchemaMock());
});
test('it invokes "fetchExceptionListItemsByListId" with expected url and body values', async () => {
@ -161,6 +368,10 @@ describe('Exceptions Lists API', () => {
http: mockKibanaHttpService(),
listId: 'myList',
namespaceType: 'single',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
@ -169,8 +380,8 @@ describe('Exceptions Lists API', () => {
query: {
list_id: 'myList',
namespace_type: 'single',
page: 1,
per_page: 20,
page: '1',
per_page: '20',
},
signal: abortCtrl.signal,
});
@ -185,6 +396,10 @@ describe('Exceptions Lists API', () => {
http: mockKibanaHttpService(),
listId: 'myList',
namespaceType: 'single',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
@ -194,8 +409,8 @@ describe('Exceptions Lists API', () => {
filter: 'exception-list.attributes.entries.field:hello world*',
list_id: 'myList',
namespace_type: 'single',
page: 1,
per_page: 20,
page: '1',
per_page: '20',
},
signal: abortCtrl.signal,
});
@ -210,6 +425,10 @@ describe('Exceptions Lists API', () => {
http: mockKibanaHttpService(),
listId: 'myList',
namespaceType: 'agnostic',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
@ -219,8 +438,8 @@ describe('Exceptions Lists API', () => {
filter: 'exception-list-agnostic.attributes.entries.field:hello world*',
list_id: 'myList',
namespace_type: 'agnostic',
page: 1,
per_page: 20,
page: '1',
per_page: '20',
},
signal: abortCtrl.signal,
});
@ -235,6 +454,10 @@ describe('Exceptions Lists API', () => {
http: mockKibanaHttpService(),
listId: 'myList',
namespaceType: 'agnostic',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
@ -244,8 +467,8 @@ describe('Exceptions Lists API', () => {
filter: 'exception-list-agnostic.attributes.tags:malware',
list_id: 'myList',
namespace_type: 'agnostic',
page: 1,
per_page: 20,
page: '1',
per_page: '20',
},
signal: abortCtrl.signal,
});
@ -260,6 +483,10 @@ describe('Exceptions Lists API', () => {
http: mockKibanaHttpService(),
listId: 'myList',
namespaceType: 'agnostic',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
@ -270,8 +497,8 @@ describe('Exceptions Lists API', () => {
'exception-list-agnostic.attributes.entries.field:host.name* AND exception-list-agnostic.attributes.tags:malware',
list_id: 'myList',
namespace_type: 'agnostic',
page: 1,
per_page: 20,
page: '1',
per_page: '20',
},
signal: abortCtrl.signal,
});
@ -282,16 +509,57 @@ describe('Exceptions Lists API', () => {
http: mockKibanaHttpService(),
listId: 'endpoint_list',
namespaceType: 'single',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]);
expect(exceptionResponse).toEqual(getFoundExceptionListItemSchemaMock());
});
test('it returns error and does not make request if request payload fails decode', async () => {
const payload = ({
http: mockKibanaHttpService(),
listId: '1',
namespaceType: 'not a namespace type',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
} as unknown) as ApiCallByListIdProps & { listId: number };
await expect(fetchExceptionListItemsByListId(payload)).rejects.toEqual(
'Invalid value "not a namespace type" supplied to "namespace_type"'
);
});
test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListItemSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
fetchExceptionListItemsByListId({
http: mockKibanaHttpService(),
listId: 'myList',
namespaceType: 'single',
pagination: {
page: 1,
perPage: 20,
},
signal: abortCtrl.signal,
})
).rejects.toEqual(
'Invalid value "undefined" supplied to "data",Invalid value "undefined" supplied to "page",Invalid value "undefined" supplied to "per_page",Invalid value "undefined" supplied to "total"'
);
});
});
describe('#fetchExceptionListItemById', () => {
beforeEach(() => {
fetchMock.mockClear();
fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]);
fetchMock.mockResolvedValue(getExceptionListItemSchemaMock());
});
test('it invokes "fetchExceptionListItemById" with expected url and body values', async () => {
@ -318,7 +586,34 @@ describe('Exceptions Lists API', () => {
namespaceType: 'single',
signal: abortCtrl.signal,
});
expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]);
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
});
test('it returns error and does not make request if request payload fails decode', async () => {
const payload = ({
http: mockKibanaHttpService(),
id: '1',
namespaceType: 'not a namespace type',
signal: abortCtrl.signal,
} as unknown) as ApiCallByIdProps & { namespaceType: string };
await expect(fetchExceptionListItemById(payload)).rejects.toEqual(
'Invalid value "not a namespace type" supplied to "namespace_type"'
);
});
test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListItemSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
fetchExceptionListItemById({
http: mockKibanaHttpService(),
id: '1',
namespaceType: 'single',
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
@ -354,6 +649,33 @@ 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 = ({
http: mockKibanaHttpService(),
id: 1,
namespaceType: 'single',
signal: abortCtrl.signal,
} as unknown) as ApiCallByIdProps & { id: number };
await expect(deleteExceptionListById(payload)).rejects.toEqual(
'Invalid value "1" supplied to "id"'
);
});
test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
deleteExceptionListById({
http: mockKibanaHttpService(),
id: '1',
namespaceType: 'single',
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
describe('#deleteExceptionListItemById', () => {
@ -388,5 +710,32 @@ describe('Exceptions Lists API', () => {
});
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
});
test('it returns error and does not make request if request payload fails decode', async () => {
const payload = ({
http: mockKibanaHttpService(),
id: 1,
namespaceType: 'single',
signal: abortCtrl.signal,
} as unknown) as ApiCallByIdProps & { id: number };
await expect(deleteExceptionListItemById(payload)).rejects.toEqual(
'Invalid value "1" supplied to "id"'
);
});
test('it returns error if response payload fails decode', async () => {
const badPayload = getExceptionListItemSchemaMock();
delete badPayload.id;
fetchMock.mockResolvedValue(badPayload);
await expect(
deleteExceptionListItemById({
http: mockKibanaHttpService(),
id: '1',
namespaceType: 'single',
signal: abortCtrl.signal,
})
).rejects.toEqual('Invalid value "undefined" supplied to "id"');
});
});
});

View file

@ -3,7 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EXCEPTION_LIST_ITEM_URL,
EXCEPTION_LIST_NAMESPACE,
@ -14,17 +13,32 @@ import {
ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
createExceptionListItemSchema,
createExceptionListSchema,
deleteExceptionListItemSchema,
deleteExceptionListSchema,
exceptionListItemSchema,
exceptionListSchema,
findExceptionListItemSchema,
foundExceptionListItemSchema,
readExceptionListItemSchema,
readExceptionListSchema,
updateExceptionListItemSchema,
updateExceptionListSchema,
} from '../../common/schemas';
import { validate } from '../../common/siem_common_deps';
import {
AddExceptionListItemProps,
AddExceptionListProps,
ApiCallByIdProps,
ApiCallByListIdProps,
UpdateExceptionListItemProps,
UpdateExceptionListProps,
} from './types';
/**
* Add provided ExceptionList
* Add new ExceptionList
*
* @param http Kibana http service
* @param list exception list to add
@ -32,22 +46,39 @@ import {
*
* @throws An error if response is not OK
*
* Uses type assertion (list as ExceptionListSchema)
* per suggestion in Typescript union docs
*/
export const addExceptionList = async ({
http,
list,
signal,
}: AddExceptionListProps): Promise<ExceptionListSchema> =>
http.fetch<ExceptionListSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(list),
method: (list as ExceptionListSchema).id != null ? 'PUT' : 'POST',
signal,
});
}: AddExceptionListProps): Promise<ExceptionListSchema> => {
const [validatedRequest, errorsRequest] = validate(list, createExceptionListSchema);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(list),
method: 'POST',
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Add provided ExceptionListItem
* Add new ExceptionListItem
*
* @param http Kibana http service
* @param listItem exception list item to add
@ -55,19 +86,116 @@ export const addExceptionList = async ({
*
* @throws An error if response is not OK
*
* Uses type assertion (listItem as ExceptionListItemSchema)
* per suggestion in Typescript union docs
*/
export const addExceptionListItem = async ({
http,
listItem,
signal,
}: AddExceptionListItemProps): Promise<ExceptionListItemSchema> =>
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
body: JSON.stringify(listItem),
method: (listItem as ExceptionListItemSchema).id != null ? 'PUT' : 'POST',
signal,
});
}: AddExceptionListItemProps): Promise<ExceptionListItemSchema> => {
const [validatedRequest, errorsRequest] = validate(listItem, createExceptionListItemSchema);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
body: JSON.stringify(listItem),
method: 'POST',
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Update existing ExceptionList
*
* @param http Kibana http service
* @param list exception list to add
* @param signal to cancel request
*
* @throws An error if response is not OK
*
*/
export const updateExceptionList = async ({
http,
list,
signal,
}: UpdateExceptionListProps): Promise<ExceptionListSchema> => {
const [validatedRequest, errorsRequest] = validate(list, updateExceptionListSchema);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(list),
method: 'PUT',
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Update existing ExceptionListItem
*
* @param http Kibana http service
* @param listItem exception list item to add
* @param signal to cancel request
*
* @throws An error if response is not OK
*
*/
export const updateExceptionListItem = async ({
http,
listItem,
signal,
}: UpdateExceptionListItemProps): Promise<ExceptionListItemSchema> => {
const [validatedRequest, errorsRequest] = validate(listItem, updateExceptionListItemSchema);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
body: JSON.stringify(listItem),
method: 'PUT',
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Fetch an ExceptionList by providing a ExceptionList ID
@ -84,12 +212,34 @@ export const fetchExceptionListById = async ({
id,
namespaceType,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> =>
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}`, {
method: 'GET',
query: { id, namespace_type: namespaceType },
signal,
});
}: ApiCallByIdProps): Promise<ExceptionListSchema> => {
const [validatedRequest, errorsRequest] = validate(
{ id, namespace_type: namespaceType },
readExceptionListSchema
);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListSchema>(EXCEPTION_LIST_URL, {
method: 'GET',
query: { id, namespace_type: namespaceType },
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id
@ -111,11 +261,7 @@ export const fetchExceptionListItemsByListId = async ({
filter: '',
tags: [],
},
pagination = {
page: 1,
perPage: 20,
total: 0,
},
pagination,
signal,
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> => {
const namespace =
@ -124,22 +270,44 @@ export const fetchExceptionListItemsByListId = async ({
...(filterOptions.filter.length
? [`${namespace}.attributes.entries.field:${filterOptions.filter}*`]
: []),
...(filterOptions.tags?.map((t) => `${namespace}.attributes.tags:${t}`) ?? []),
...(filterOptions.tags.length
? filterOptions.tags.map((t) => `${namespace}.attributes.tags:${t}`)
: []),
];
const query = {
list_id: listId,
namespace_type: namespaceType,
page: pagination.page,
per_page: pagination.perPage,
page: pagination.page ? `${pagination.page}` : '1',
per_page: pagination.perPage ? `${pagination.perPage}` : '20',
...(filters.length ? { filter: filters.join(' AND ') } : {}),
};
const [validatedRequest, errorsRequest] = validate(query, findExceptionListItemSchema);
return http.fetch<FoundExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}/_find`, {
method: 'GET',
query,
signal,
});
if (validatedRequest != null) {
try {
const response = await http.fetch<FoundExceptionListItemSchema>(
`${EXCEPTION_LIST_ITEM_URL}/_find`,
{
method: 'GET',
query,
signal,
}
);
const [validatedResponse, errorsResponse] = validate(response, foundExceptionListItemSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
@ -157,12 +325,33 @@ export const fetchExceptionListItemById = async ({
id,
namespaceType,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
method: 'GET',
query: { id, namespace_type: namespaceType },
signal,
});
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> => {
const [validatedRequest, errorsRequest] = validate(
{ id, namespace_type: namespaceType },
readExceptionListItemSchema
);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
method: 'GET',
query: { id, namespace_type: namespaceType },
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Delete an ExceptionList by providing a ExceptionList ID
@ -179,12 +368,34 @@ export const deleteExceptionListById = async ({
id,
namespaceType,
signal,
}: ApiCallByIdProps): Promise<ExceptionListSchema> =>
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}`, {
method: 'DELETE',
query: { id, namespace_type: namespaceType },
signal,
});
}: ApiCallByIdProps): Promise<ExceptionListSchema> => {
const [validatedRequest, errorsRequest] = validate(
{ id, namespace_type: namespaceType },
deleteExceptionListSchema
);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListSchema>(EXCEPTION_LIST_URL, {
method: 'DELETE',
query: { id, namespace_type: namespaceType },
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};
/**
* Delete an ExceptionListItem by providing a ExceptionListItem ID
@ -201,9 +412,31 @@ export const deleteExceptionListItemById = async ({
id,
namespaceType,
signal,
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
method: 'DELETE',
query: { id, namespace_type: namespaceType },
signal,
});
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> => {
const [validatedRequest, errorsRequest] = validate(
{ id, namespace_type: namespaceType },
deleteExceptionListItemSchema
);
if (validatedRequest != null) {
try {
const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
method: 'DELETE',
query: { id, namespace_type: namespaceType },
signal,
});
const [validatedResponse, errorsResponse] = validate(response, exceptionListItemSchema);
if (errorsResponse != null || validatedResponse == null) {
return Promise.reject(errorsResponse);
} else {
return Promise.resolve(validatedResponse);
}
} catch (error) {
return Promise.reject(error);
}
} else {
return Promise.reject(errorsRequest);
}
};

View file

@ -7,19 +7,24 @@
import { act, renderHook } from '@testing-library/react-hooks';
import * as api from '../api';
import { getCreateExceptionListItemSchemaMock } from '../../../common/schemas/request/create_exception_list_item_schema.mock';
import { getUpdateExceptionListItemSchemaMock } from '../../../common/schemas/request/update_exception_list_item_schema.mock';
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
import { PersistHookProps } from '../types';
import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('usePersistExceptionItem', () => {
const onError = jest.fn();
beforeEach(() => {
jest.spyOn(api, 'addExceptionListItem').mockResolvedValue(getExceptionListItemSchemaMock());
jest.spyOn(api, 'updateExceptionListItem').mockResolvedValue(getExceptionListItemSchemaMock());
});
afterEach(() => {
jest.clearAllMocks();
});
@ -40,7 +45,7 @@ describe('usePersistExceptionItem', () => {
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getExceptionListItemSchemaMock());
result.current[1](getCreateExceptionListItemSchemaMock());
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
@ -55,13 +60,32 @@ describe('usePersistExceptionItem', () => {
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getExceptionListItemSchemaMock());
result.current[1](getCreateExceptionListItemSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
});
test('it invokes "updateExceptionListItem" when payload has "id"', async () => {
const addExceptionItem = jest.spyOn(api, 'addExceptionListItem');
const updateExceptionItem = jest.spyOn(api, 'updateExceptionListItem');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionItem
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getUpdateExceptionListItemSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
expect(addExceptionItem).not.toHaveBeenCalled();
expect(updateExceptionItem).toHaveBeenCalled();
});
});
test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => {
const error = new Error('persist rule failed');
jest.spyOn(api, 'addExceptionListItem').mockRejectedValue(error);
@ -73,7 +97,7 @@ describe('usePersistExceptionItem', () => {
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getExceptionListItemSchemaMock());
result.current[1](getCreateExceptionListItemSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);

View file

@ -3,10 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Dispatch, useEffect, useState } from 'react';
import { addExceptionListItem as persistExceptionItem } from '../api';
import { UpdateExceptionListItemSchema } from '../../../common/schemas';
import { addExceptionListItem, updateExceptionListItem } from '../api';
import { AddExceptionListItem, PersistHookProps } from '../types';
interface PersistReturnExceptionItem {
@ -33,6 +33,8 @@ export const usePersistExceptionItem = ({
const [exceptionListItem, setExceptionItem] = useState<AddExceptionListItem | null>(null);
const [isSaved, setIsSaved] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const isUpdateExceptionItem = (item: unknown): item is UpdateExceptionListItemSchema =>
Boolean(item && (item as UpdateExceptionListItemSchema).id != null);
useEffect(() => {
let isSubscribed = true;
@ -43,11 +45,20 @@ export const usePersistExceptionItem = ({
if (exceptionListItem != null) {
try {
setIsLoading(true);
await persistExceptionItem({
http,
listItem: exceptionListItem,
signal: abortCtrl.signal,
});
if (isUpdateExceptionItem(exceptionListItem)) {
await updateExceptionListItem({
http,
listItem: exceptionListItem,
signal: abortCtrl.signal,
});
} else {
await addExceptionListItem({
http,
listItem: exceptionListItem,
signal: abortCtrl.signal,
});
}
if (isSubscribed) {
setIsSaved(true);
}

View file

@ -7,19 +7,24 @@
import { act, renderHook } from '@testing-library/react-hooks';
import * as api from '../api';
import { getCreateExceptionListSchemaMock } from '../../../common/schemas/request/create_exception_list_schema.mock';
import { getUpdateExceptionListSchemaMock } from '../../../common/schemas/request/update_exception_list_schema.mock';
import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
import { PersistHookProps } from '../types';
import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('usePersistExceptionList', () => {
const onError = jest.fn();
beforeEach(() => {
jest.spyOn(api, 'addExceptionList').mockResolvedValue(getExceptionListSchemaMock());
jest.spyOn(api, 'updateExceptionList').mockResolvedValue(getExceptionListSchemaMock());
});
afterEach(() => {
jest.clearAllMocks();
});
@ -39,7 +44,7 @@ describe('usePersistExceptionList', () => {
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getExceptionListSchemaMock());
result.current[1](getCreateExceptionListSchemaMock());
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
@ -53,13 +58,32 @@ describe('usePersistExceptionList', () => {
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getExceptionListSchemaMock());
result.current[1](getCreateExceptionListSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
});
test('it invokes "updateExceptionList" when payload has "id"', async () => {
const addException = jest.spyOn(api, 'addExceptionList');
const updateException = jest.spyOn(api, 'updateExceptionList');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getUpdateExceptionListSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
expect(addException).not.toHaveBeenCalled();
expect(updateException).toHaveBeenCalled();
});
});
test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => {
const error = new Error('persist rule failed');
jest.spyOn(api, 'addExceptionList').mockRejectedValue(error);
@ -70,7 +94,7 @@ describe('usePersistExceptionList', () => {
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getExceptionListSchemaMock());
result.current[1](getCreateExceptionListSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);

View file

@ -6,7 +6,8 @@
import { Dispatch, useEffect, useState } from 'react';
import { addExceptionList as persistExceptionList } from '../api';
import { UpdateExceptionListSchema } from '../../../common/schemas';
import { addExceptionList, updateExceptionList } from '../api';
import { AddExceptionList, PersistHookProps } from '../types';
interface PersistReturnExceptionList {
@ -33,6 +34,8 @@ export const usePersistExceptionList = ({
const [exceptionList, setExceptionList] = useState<AddExceptionList | null>(null);
const [isSaved, setIsSaved] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const isUpdateExceptionList = (item: unknown): item is UpdateExceptionListSchema =>
Boolean(item && (item as UpdateExceptionListSchema).id != null);
useEffect(() => {
let isSubscribed = true;
@ -43,7 +46,11 @@ export const usePersistExceptionList = ({
if (exceptionList != null) {
try {
setIsLoading(true);
await persistExceptionList({ http, list: exceptionList, signal: abortCtrl.signal });
if (isUpdateExceptionList(exceptionList)) {
await updateExceptionList({ http, list: exceptionList, signal: abortCtrl.signal });
} else {
await addExceptionList({ http, list: exceptionList, signal: abortCtrl.signal });
}
if (isSubscribed) {
setIsSaved(true);
}

View file

@ -15,8 +15,6 @@ import { ApiCallByIdProps } from '../types';
import { ExceptionsApi, useApi } from './use_api';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('useApi', () => {

View file

@ -9,19 +9,24 @@ import { act, renderHook } from '@testing-library/react-hooks';
import * as api from '../api';
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock';
import { ExceptionListItemSchema } from '../../../common/schemas';
import { ExceptionList, UseExceptionListProps, UseExceptionListSuccess } from '../types';
import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list';
jest.mock('../api');
const mockKibanaHttpService = createKibanaCoreStartMock().http;
describe('useExceptionList', () => {
const onErrorMock = jest.fn();
beforeEach(() => {
jest.spyOn(api, 'fetchExceptionListById').mockResolvedValue(getExceptionListSchemaMock());
jest
.spyOn(api, 'fetchExceptionListItemsByListId')
.mockResolvedValue(getFoundExceptionListItemSchemaMock());
});
afterEach(() => {
onErrorMock.mockClear();
jest.clearAllMocks();
@ -34,9 +39,15 @@ describe('useExceptionList', () => {
ReturnExceptionListAndItems
>(() =>
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single' }],
onError: onErrorMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
})
);
await waitForNextUpdate();
@ -63,10 +74,16 @@ describe('useExceptionList', () => {
ReturnExceptionListAndItems
>(() =>
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single' }],
onError: onErrorMock,
onSuccess: onSuccessMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
})
);
await waitForNextUpdate();
@ -76,14 +93,12 @@ describe('useExceptionList', () => {
{ ...getExceptionListSchemaMock(), totalItems: 1 },
];
const expectedListItemsResult: ExceptionListItemSchema[] = [
{ ...getExceptionListItemSchemaMock() },
];
const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock()
.data;
const expectedResult: UseExceptionListSuccess = {
exceptions: expectedListItemsResult,
lists: expectedListResult,
pagination: { page: 1, perPage: 20, total: 1 },
pagination: { page: 1, perPage: 1, total: 1 },
};
expect(result.current).toEqual([
@ -92,7 +107,7 @@ describe('useExceptionList', () => {
expectedListItemsResult,
{
page: 1,
perPage: 20,
perPage: 1,
total: 1,
},
result.current[4],
@ -104,6 +119,7 @@ describe('useExceptionList', () => {
test('fetch a new exception list and its items', async () => {
const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById');
const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId');
const onSuccessMock = jest.fn();
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<
UseExceptionListProps,
@ -113,18 +129,31 @@ describe('useExceptionList', () => {
useExceptionList({ filterOptions, http, lists, onError, onSuccess, pagination }),
{
initialProps: {
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single' }],
onError: onErrorMock,
onSuccess: jest.fn(),
onSuccess: onSuccessMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
},
}
);
await waitForNextUpdate();
rerender({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'newListId', namespaceType: 'single' }],
onError: onErrorMock,
onSuccess: onSuccessMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
});
await waitForNextUpdate();
@ -142,9 +171,15 @@ describe('useExceptionList', () => {
ReturnExceptionListAndItems
>(() =>
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single' }],
onError: onErrorMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
})
);
await waitForNextUpdate();
@ -173,9 +208,15 @@ describe('useExceptionList', () => {
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
() =>
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single' }],
onError: onErrorMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
})
);
await waitForNextUpdate();
@ -195,9 +236,15 @@ describe('useExceptionList', () => {
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
() =>
useExceptionList({
filterOptions: { filter: '', tags: [] },
http: mockKibanaHttpService,
lists: [{ id: 'myListId', namespaceType: 'single' }],
onError: onErrorMock,
pagination: {
page: 1,
perPage: 20,
total: 0,
},
})
);
await waitForNextUpdate();

View file

@ -137,7 +137,9 @@ export const useExceptionList = ({
perPage: 20,
total: 0,
});
onError(error);
if (onError != null) {
onError(error);
}
}
}
};

View file

@ -1,42 +0,0 @@
/*
* 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 {
CreateExceptionListItemSchemaPartial,
CreateExceptionListSchemaPartial,
} from '../../common/schemas';
export const mockNewExceptionList: CreateExceptionListSchemaPartial = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
description: 'This is a sample endpoint type exception',
list_id: 'endpoint_list',
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
type: 'endpoint',
};
export const mockNewExceptionItem: CreateExceptionListItemSchemaPartial = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
description: 'This is a sample endpoint type exception',
entries: [
{
field: 'actingProcess.file.signer',
match: 'Elastic, N.V.',
match_any: undefined,
operator: 'included',
},
{
field: 'event.category',
match: undefined,
match_any: ['process', 'malware'],
operator: 'included',
},
],
item_id: 'endpoint_list_item',
list_id: 'endpoint_list',
name: 'Sample Endpoint Exception List',
tags: ['user added string for a tag', 'malware'],
type: 'simple',
};

View file

@ -5,11 +5,16 @@
*/
import {
CreateExceptionListItemSchemaPartial,
CreateExceptionListSchemaPartial,
CreateExceptionListItemSchema,
CreateExceptionListSchema,
ExceptionListItemSchema,
ExceptionListSchema,
NamespaceType,
Page,
PerPage,
TotalOrUndefined,
UpdateExceptionListItemSchema,
UpdateExceptionListSchema,
} from '../../common/schemas';
import { HttpStart } from '../../../../../src/core/public';
@ -19,14 +24,14 @@ export interface FilterExceptionsOptions {
}
export interface Pagination {
page: number;
perPage: number;
total: number;
page: Page;
perPage: PerPage;
total: TotalOrUndefined;
}
export type AddExceptionList = ExceptionListSchema | CreateExceptionListSchemaPartial;
export type AddExceptionList = UpdateExceptionListSchema | CreateExceptionListSchema;
export type AddExceptionListItem = CreateExceptionListItemSchemaPartial | ExceptionListItemSchema;
export type AddExceptionListItem = CreateExceptionListItemSchema | UpdateExceptionListItemSchema;
export interface PersistHookProps {
http: HttpStart;
@ -46,7 +51,7 @@ export interface UseExceptionListSuccess {
export interface UseExceptionListProps {
http: HttpStart;
lists: ExceptionIdentifiers[];
onError: (arg: Error) => void;
onError?: (arg: string[]) => void;
filterOptions?: FilterExceptionsOptions;
pagination?: Pagination;
onSuccess?: (arg: UseExceptionListSuccess) => void;
@ -63,7 +68,7 @@ export interface ApiCallByListIdProps {
listId: string;
namespaceType: NamespaceType;
filterOptions?: FilterExceptionsOptions;
pagination?: Pagination;
pagination: Partial<Pagination>;
signal: AbortSignal;
}
@ -77,18 +82,30 @@ export interface ApiCallByIdProps {
export interface ApiCallMemoProps {
id: string;
namespaceType: NamespaceType;
onError: (arg: Error) => void;
onError: (arg: string[]) => void;
onSuccess: () => void;
}
export interface AddExceptionListProps {
http: HttpStart;
list: AddExceptionList;
list: CreateExceptionListSchema;
signal: AbortSignal;
}
export interface AddExceptionListItemProps {
http: HttpStart;
listItem: AddExceptionListItem;
listItem: CreateExceptionListItemSchema;
signal: AbortSignal;
}
export interface UpdateExceptionListProps {
http: HttpStart;
list: UpdateExceptionListSchema;
signal: AbortSignal;
}
export interface UpdateExceptionListItemProps {
http: HttpStart;
listItem: UpdateExceptionListItemSchema;
signal: AbortSignal;
}

View file

@ -8,5 +8,9 @@ export { useApi } from './exceptions/hooks/use_api';
export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item';
export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list';
export { useExceptionList } from './exceptions/hooks/use_exception_list';
export { ExceptionList, ExceptionIdentifiers } from './exceptions/types';
export { mockNewExceptionItem, mockNewExceptionList } from './exceptions/mock';
export {
ExceptionList,
ExceptionIdentifiers,
Pagination,
UseExceptionListSuccess,
} from './exceptions/types';

View file

@ -81,18 +81,44 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
},
entries: {
properties: {
entries: {
properties: {
field: {
type: 'keyword',
},
operator: {
type: 'keyword',
},
type: {
type: 'keyword',
},
value: {
fields: {
text: {
type: 'text',
},
},
type: 'keyword',
},
},
},
field: {
type: 'keyword',
},
match: {
type: 'keyword',
},
match_any: {
type: 'keyword',
},
operator: {
type: 'keyword',
},
type: {
type: 'keyword',
},
value: {
fields: {
text: {
type: 'text',
},
},
type: 'keyword',
},
},
},
item_id: {

View file

@ -9,13 +9,14 @@
"entries": [
{
"field": "actingProcess.file.signer",
"operator": "included",
"match": "Elastic, N.V."
"operator": "excluded",
"type": "exists"
},
{
"field": "event.category",
"field": "host.name",
"operator": "included",
"match_any": ["process", "malware"]
"type": "match_any",
"value": ["some host", "another host"]
}
]
}

View file

@ -11,12 +11,26 @@
{
"field": "actingProcess.file.signer",
"operator": "included",
"match": "Elastic, N.V."
"type": "match",
"value": "Elastic, N.V."
},
{
"field": "event.category",
"operator": "included",
"match_any": ["process", "malware"]
"field": "file.signature",
"type": "nested",
"entries": [
{
"field": "signer",
"type": "match",
"operator": "included",
"value": "Evil"
},
{
"field": "trusted",
"type": "match",
"operator": "included",
"value": "true"
}
]
}
]
}

View file

@ -10,12 +10,26 @@
{
"field": "actingProcess.file.signer",
"operator": "included",
"match": "Elastic, N.V."
"type": "match",
"value": "Elastic, N.V."
},
{
"field": "event.category",
"operator": "included",
"match_any": ["process", "malware"]
"field": "file.signature",
"type": "nested",
"entries": [
{
"field": "signer",
"type": "match",
"operator": "included",
"value": "Evil"
},
{
"field": "trusted",
"type": "match",
"operator": "included",
"value": "true"
}
]
}
]
}

View file

@ -7,20 +7,34 @@
"name": "Sample Detection Exception List Item",
"comments": [{ "comment": "This is a short little comment." }],
"entries": [
{
"field": "actingProcess.file.signer",
"operator": "excluded",
"type": "exists"
},
{
"field": "host.name",
"operator": "included",
"match": "sampleHostName"
"type": "match_any",
"value": ["some host", "another host"]
},
{
"field": "event.category",
"operator": "included",
"match_any": ["process", "malware"]
},
{
"field": "event.action",
"operator": "included",
"match": "user-password-change"
"field": "file.signature",
"type": "nested",
"entries": [
{
"field": "signer",
"type": "match",
"operator": "included",
"value": "Evil"
},
{
"field": "trusted",
"type": "match",
"operator": "included",
"value": "true"
}
]
}
]
}

View file

@ -10,7 +10,8 @@
{
"field": "event.category",
"operator": "included",
"match_any": ["process", "malware"]
"type": "match_any",
"value": ["process", "malware"]
}
]
}

View file

@ -17,7 +17,8 @@
{
"field": "event.category",
"operator": "included",
"match_any": ["process", "malware"]
"type": "match_any",
"value": ["process", "malware"]
}
]
}

View file

@ -0,0 +1,14 @@
/*
* 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 { FormattedEntry } from './types';
export const getFormattedEntryMock = (isNested = false): FormattedEntry => ({
fieldName: 'host.name',
operator: 'is',
value: 'some name',
isNested,
});

View file

@ -10,7 +10,6 @@ import moment from 'moment-timezone';
import {
getOperatorType,
getExceptionOperatorSelect,
isEntryNested,
getFormattedEntries,
formatEntry,
getOperatingSystems,
@ -18,13 +17,7 @@ import {
getDescriptionListContent,
getFormattedComments,
} from './helpers';
import {
OperatorType,
Operator,
NestedExceptionEntry,
FormattedEntry,
DescriptionListItem,
} from './types';
import { FormattedEntry, DescriptionListItem } from './types';
import {
isOperator,
isNotOperator,
@ -35,7 +28,16 @@ import {
existsOperator,
doesNotExistOperator,
} from './operators';
import { getExceptionItemEntryMock, getExceptionItemMock } from './mocks';
import { OperatorTypeEnum } from '../../../lists_plugin_deps';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import {
getEntryExistsMock,
getEntryListMock,
getEntryMatchMock,
getEntryMatchAnyMock,
getEntriesArrayMock,
} from '../../../../../lists/common/schemas/types/entries.mock';
import { getCommentsMock } from '../../../../../lists/common/schemas/types/comments.mock';
describe('Exception helpers', () => {
beforeEach(() => {
@ -48,137 +50,96 @@ describe('Exception helpers', () => {
describe('#getOperatorType', () => {
test('returns operator type "match" if entry.type is "match"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'match';
const payload = getEntryMatchMock();
const operatorType = getOperatorType(payload);
expect(operatorType).toEqual(OperatorType.PHRASE);
});
test('returns operator type "match" if entry.type is "nested"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'nested';
const operatorType = getOperatorType(payload);
expect(operatorType).toEqual(OperatorType.PHRASE);
expect(operatorType).toEqual(OperatorTypeEnum.MATCH);
});
test('returns operator type "match_any" if entry.type is "match_any"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'match_any';
const payload = getEntryMatchAnyMock();
const operatorType = getOperatorType(payload);
expect(operatorType).toEqual(OperatorType.PHRASES);
expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY);
});
test('returns operator type "list" if entry.type is "list"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'list';
const payload = getEntryListMock();
const operatorType = getOperatorType(payload);
expect(operatorType).toEqual(OperatorType.LIST);
expect(operatorType).toEqual(OperatorTypeEnum.LIST);
});
test('returns operator type "exists" if entry.type is "exists"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'exists';
const payload = getEntryExistsMock();
const operatorType = getOperatorType(payload);
expect(operatorType).toEqual(OperatorType.EXISTS);
expect(operatorType).toEqual(OperatorTypeEnum.EXISTS);
});
});
describe('#getExceptionOperatorSelect', () => {
test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => {
const payload = getExceptionItemEntryMock();
const payload = getEntryMatchMock();
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(isOperator);
});
test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => {
const payload = getExceptionItemEntryMock();
payload.operator = Operator.EXCLUSION;
const payload = getEntryMatchMock();
payload.operator = 'excluded';
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(isNotOperator);
});
test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'match_any';
payload.operator = Operator.INCLUSION;
const payload = getEntryMatchAnyMock();
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(isOneOfOperator);
});
test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'match_any';
payload.operator = Operator.EXCLUSION;
const payload = getEntryMatchAnyMock();
payload.operator = 'excluded';
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(isNotOneOfOperator);
});
test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'exists';
payload.operator = Operator.INCLUSION;
const payload = getEntryExistsMock();
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(existsOperator);
});
test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'exists';
payload.operator = Operator.EXCLUSION;
const payload = getEntryExistsMock();
payload.operator = 'excluded';
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(doesNotExistOperator);
});
test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'list';
payload.operator = Operator.INCLUSION;
const payload = getEntryListMock();
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(isInListOperator);
});
test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => {
const payload = getExceptionItemEntryMock();
payload.type = 'list';
payload.operator = Operator.EXCLUSION;
const payload = getEntryListMock();
payload.operator = 'excluded';
const result = getExceptionOperatorSelect(payload);
expect(result).toEqual(isNotInListOperator);
});
});
describe('#isEntryNested', () => {
test('it returns true if type NestedExceptionEntry', () => {
const payload: NestedExceptionEntry = {
field: 'actingProcess.file.signer',
type: 'nested',
entries: [],
};
const result = isEntryNested(payload);
expect(result).toBeTruthy();
});
test('it returns false if NOT type NestedExceptionEntry', () => {
const payload = getExceptionItemEntryMock();
const result = isEntryNested(payload);
expect(result).toBeFalsy();
});
});
describe('#getFormattedEntries', () => {
test('it returns empty array if no entries passed', () => {
const result = getFormattedEntries([]);
@ -187,116 +148,98 @@ describe('Exception helpers', () => {
});
test('it formats nested entries as expected', () => {
const payload = [
{
field: 'file.signature',
type: 'nested',
entries: [
{
field: 'signer',
type: 'match',
operator: Operator.INCLUSION,
value: 'Evil',
},
{
field: 'trusted',
type: 'match',
operator: Operator.INCLUSION,
value: 'true',
},
],
},
];
const payload = [getEntryMatchMock()];
const result = getFormattedEntries(payload);
const expected: FormattedEntry[] = [
{
fieldName: 'file.signature',
operator: null,
value: null,
fieldName: 'host.name',
isNested: false,
},
{
fieldName: 'file.signature.signer',
isNested: true,
operator: 'is',
value: 'Evil',
value: 'some host name',
},
];
expect(result).toEqual(expected);
});
test('it formats "exists" entries as expected', () => {
const payload = [getEntryExistsMock()];
const result = getFormattedEntries(payload);
const expected: FormattedEntry[] = [
{
fieldName: 'file.signature.trusted',
isNested: true,
operator: 'is',
value: 'true',
fieldName: 'host.name',
isNested: false,
operator: 'exists',
value: null,
},
];
expect(result).toEqual(expected);
});
test('it formats non-nested entries as expected', () => {
const payload = [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.INCLUSION,
value: 'Elastic, N.V.',
},
{
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.EXCLUSION,
value: 'Global Signer',
},
];
const payload = [getEntryMatchAnyMock(), getEntryMatchMock()];
const result = getFormattedEntries(payload);
const expected: FormattedEntry[] = [
{
fieldName: 'actingProcess.file.signer',
fieldName: 'host.name',
isNested: false,
operator: 'is',
value: 'Elastic, N.V.',
operator: 'is one of',
value: ['some host name'],
},
{
fieldName: 'actingProcess.file.signer',
fieldName: 'host.name',
isNested: false,
operator: 'is not',
value: 'Global Signer',
operator: 'is',
value: 'some host name',
},
];
expect(result).toEqual(expected);
});
test('it formats a mix of nested and non-nested entries as expected', () => {
const payload = getExceptionItemMock();
const result = getFormattedEntries(payload.entries);
const payload = getEntriesArrayMock();
const result = getFormattedEntries(payload);
const expected: FormattedEntry[] = [
{
fieldName: 'actingProcess.file.signer',
fieldName: 'host.name',
isNested: false,
operator: 'is',
value: 'Elastic, N.V.',
value: 'some host name',
},
{
fieldName: 'host.name',
isNested: false,
operator: 'is not',
value: 'Global Signer',
operator: 'is one of',
value: ['some host name'],
},
{
fieldName: 'file.signature',
fieldName: 'host.name',
isNested: false,
operator: 'is in list',
value: ['some host name'],
},
{
fieldName: 'host.name',
isNested: false,
operator: 'exists',
value: null,
},
{
fieldName: 'host.name',
isNested: false,
operator: null,
value: null,
},
{
fieldName: 'file.signature.signer',
fieldName: 'host.name.host.name',
isNested: true,
operator: 'is',
value: 'Evil',
value: 'some host name',
},
{
fieldName: 'file.signature.trusted',
fieldName: 'host.name.host.name',
isNested: true,
operator: 'is',
value: 'true',
operator: 'exists',
value: null,
},
];
expect(result).toEqual(expected);
@ -305,26 +248,26 @@ describe('Exception helpers', () => {
describe('#formatEntry', () => {
test('it formats an entry', () => {
const payload = getExceptionItemEntryMock();
const payload = getEntryMatchMock();
const formattedEntry = formatEntry({ isNested: false, item: payload });
const expected: FormattedEntry = {
fieldName: 'actingProcess.file.signer',
fieldName: 'host.name',
isNested: false,
operator: 'is',
value: 'Elastic, N.V.',
value: 'some host name',
};
expect(formattedEntry).toEqual(expected);
});
test('it formats a nested entry', () => {
const payload = getExceptionItemEntryMock();
test('it formats as expected when "isNested" is "true"', () => {
const payload = getEntryMatchMock();
const formattedEntry = formatEntry({ isNested: true, parent: 'parent', item: payload });
const expected: FormattedEntry = {
fieldName: 'parent.actingProcess.file.signer',
fieldName: 'parent.host.name',
isNested: true,
operator: 'is',
value: 'Elastic, N.V.',
value: 'some host name',
};
expect(formattedEntry).toEqual(expected);
@ -373,12 +316,12 @@ describe('Exception helpers', () => {
describe('#getDescriptionListContent', () => {
test('it returns formatted description list with os if one is specified', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload.description = '';
const result = getDescriptionListContent(payload);
const expected: DescriptionListItem[] = [
{
description: 'Windows',
description: 'Linux',
title: 'OS',
},
{
@ -395,7 +338,7 @@ describe('Exception helpers', () => {
});
test('it returns formatted description list with a description if one specified', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = 'Im a description';
const result = getDescriptionListContent(payload);
@ -418,7 +361,7 @@ describe('Exception helpers', () => {
});
test('it returns just user and date created if no other fields specified', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = '';
const result = getDescriptionListContent(payload);
@ -439,29 +382,29 @@ describe('Exception helpers', () => {
describe('#getFormattedComments', () => {
test('it returns formatted comment object with username and timestamp', () => {
const payload = getExceptionItemMock().comments;
const payload = getCommentsMock();
const result = getFormattedComments(payload);
expect(result[0].username).toEqual('user_name');
expect(result[0].timestamp).toEqual('on Apr 23rd 2020 @ 00:19:13');
expect(result[0].username).toEqual('some user');
expect(result[0].timestamp).toEqual('on Apr 20th 2020 @ 15:25:31');
});
test('it returns formatted timeline icon with comment users initial', () => {
const payload = getExceptionItemMock().comments;
const payload = getCommentsMock();
const result = getFormattedComments(payload);
const wrapper = mount<React.ReactElement>(result[0].timelineIcon as React.ReactElement);
expect(wrapper.text()).toEqual('U');
expect(wrapper.text()).toEqual('SU');
});
test('it returns comment text', () => {
const payload = getExceptionItemMock().comments;
const payload = getCommentsMock();
const result = getFormattedComments(payload);
const wrapper = mount<React.ReactElement>(result[0].children as React.ReactElement);
expect(wrapper.text()).toEqual('Comment goes here');
expect(wrapper.text()).toEqual('some comment');
});
});
});

View file

@ -10,34 +10,32 @@ import { capitalize } from 'lodash';
import moment from 'moment';
import * as i18n from './translations';
import {
FormattedEntry,
OperatorType,
OperatorOption,
ExceptionEntry,
NestedExceptionEntry,
DescriptionListItem,
Comment,
ExceptionListItemSchema,
} from './types';
import { FormattedEntry, OperatorOption, DescriptionListItem, Comment } from './types';
import { EXCEPTION_OPERATORS, isOperator } from './operators';
import {
Entry,
EntriesArray,
ExceptionListItemSchema,
OperatorTypeEnum,
entriesNested,
entriesExists,
} from '../../../lists_plugin_deps';
/**
* Returns the operator type, may not need this if using io-ts types
*
* @param entry a single ExceptionItem entry
*/
export const getOperatorType = (entry: ExceptionEntry): OperatorType => {
export const getOperatorType = (entry: Entry): OperatorTypeEnum => {
switch (entry.type) {
case 'nested':
case 'match':
return OperatorType.PHRASE;
return OperatorTypeEnum.MATCH;
case 'match_any':
return OperatorType.PHRASES;
return OperatorTypeEnum.MATCH_ANY;
case 'list':
return OperatorType.LIST;
return OperatorTypeEnum.LIST;
default:
return OperatorType.EXISTS;
return OperatorTypeEnum.EXISTS;
}
};
@ -47,22 +45,17 @@ export const getOperatorType = (entry: ExceptionEntry): OperatorType => {
*
* @param entry a single ExceptionItem entry
*/
export const getExceptionOperatorSelect = (entry: ExceptionEntry): OperatorOption => {
const operatorType = getOperatorType(entry);
const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => {
return entry.operator === operatorOption.operator && operatorType === operatorOption.type;
});
export const getExceptionOperatorSelect = (entry: Entry): OperatorOption => {
if (entriesNested.is(entry)) {
return isOperator;
} else {
const operatorType = getOperatorType(entry);
const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => {
return entry.operator === operatorOption.operator && operatorType === operatorOption.type;
});
return foundOperator ?? isOperator;
};
export const isEntryNested = (
tbd: ExceptionEntry | NestedExceptionEntry
): tbd is NestedExceptionEntry => {
if (tbd.type === 'nested') {
return true;
return foundOperator ?? isOperator;
}
return false;
};
/**
@ -71,11 +64,9 @@ export const isEntryNested = (
*
* @param entries an ExceptionItem's entries
*/
export const getFormattedEntries = (
entries: Array<ExceptionEntry | NestedExceptionEntry>
): FormattedEntry[] => {
export const getFormattedEntries = (entries: EntriesArray): FormattedEntry[] => {
const formattedEntries = entries.map((entry) => {
if (isEntryNested(entry)) {
if (entriesNested.is(entry)) {
const parent = { fieldName: entry.field, operator: null, value: null, isNested: false };
return entry.entries.reduce<FormattedEntry[]>(
(acc, nestedEntry) => {
@ -106,11 +97,10 @@ export const formatEntry = ({
}: {
isNested: boolean;
parent?: string;
item: ExceptionEntry;
item: Entry;
}): FormattedEntry => {
const operator = getExceptionOperatorSelect(item);
const operatorType = getOperatorType(item);
const value = operatorType === OperatorType.EXISTS ? null : item.value;
const value = !entriesExists.is(item) ? item.value : null;
return {
fieldName: isNested ? `${parent}.${item.field}` : item.field,

View file

@ -1,108 +0,0 @@
/*
* 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 {
Operator,
ExceptionListItemSchema,
ExceptionEntry,
NestedExceptionEntry,
FormattedEntry,
} from './types';
import { ExceptionList } from '../../../lists_plugin_deps';
export const getExceptionListMock = (): ExceptionList => ({
id: '5b543420',
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
list_id: 'test-exception',
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
updated_at: '2020-04-23T00:19:13.289Z',
updated_by: 'user_name',
namespace_type: 'single',
name: '',
description: 'This is a description',
_tags: ['os:windows'],
tags: [],
type: 'endpoint',
meta: {},
totalItems: 0,
});
export const getExceptionItemEntryMock = (): ExceptionEntry => ({
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.INCLUSION,
value: 'Elastic, N.V.',
});
export const getNestedExceptionItemEntryMock = (): NestedExceptionEntry => ({
field: 'actingProcess.file.signer',
type: 'nested',
entries: [{ ...getExceptionItemEntryMock() }],
});
export const getFormattedEntryMock = (isNested = false): FormattedEntry => ({
fieldName: 'host.name',
operator: 'is',
value: 'some name',
isNested,
});
export const getExceptionItemMock = (): ExceptionListItemSchema => ({
id: 'uuid_here',
item_id: 'item-id',
created_at: '2020-04-23T00:19:13.289Z',
created_by: 'user_name',
list_id: 'test-exception',
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
updated_at: '2020-04-23T00:19:13.289Z',
updated_by: 'user_name',
namespace_type: 'single',
name: '',
description: 'This is a description',
comments: [
{
created_by: 'user_name',
created_at: '2020-04-23T00:19:13.289Z',
comment: 'Comment goes here',
},
],
_tags: ['os:windows'],
tags: [],
type: 'simple',
entries: [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.INCLUSION,
value: 'Elastic, N.V.',
},
{
field: 'host.name',
type: 'match',
operator: Operator.EXCLUSION,
value: 'Global Signer',
},
{
field: 'file.signature',
type: 'nested',
entries: [
{
field: 'signer',
type: 'match',
operator: Operator.INCLUSION,
value: 'Evil',
},
{
field: 'trusted',
type: 'match',
operator: Operator.INCLUSION,
value: 'true',
},
],
},
],
});

View file

@ -5,15 +5,16 @@
*/
import { i18n } from '@kbn/i18n';
import { OperatorOption, OperatorType, Operator } from './types';
import { OperatorOption } from './types';
import { OperatorTypeEnum } from '../../../lists_plugin_deps';
export const isOperator: OperatorOption = {
message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', {
defaultMessage: 'is',
}),
value: 'is',
type: OperatorType.PHRASE,
operator: Operator.INCLUSION,
type: OperatorTypeEnum.MATCH,
operator: 'included',
};
export const isNotOperator: OperatorOption = {
@ -21,8 +22,8 @@ export const isNotOperator: OperatorOption = {
defaultMessage: 'is not',
}),
value: 'is_not',
type: OperatorType.PHRASE,
operator: Operator.EXCLUSION,
type: OperatorTypeEnum.MATCH,
operator: 'excluded',
};
export const isOneOfOperator: OperatorOption = {
@ -30,8 +31,8 @@ export const isOneOfOperator: OperatorOption = {
defaultMessage: 'is one of',
}),
value: 'is_one_of',
type: OperatorType.PHRASES,
operator: Operator.INCLUSION,
type: OperatorTypeEnum.MATCH_ANY,
operator: 'included',
};
export const isNotOneOfOperator: OperatorOption = {
@ -39,8 +40,8 @@ export const isNotOneOfOperator: OperatorOption = {
defaultMessage: 'is not one of',
}),
value: 'is_not_one_of',
type: OperatorType.PHRASES,
operator: Operator.EXCLUSION,
type: OperatorTypeEnum.MATCH_ANY,
operator: 'excluded',
};
export const existsOperator: OperatorOption = {
@ -48,8 +49,8 @@ export const existsOperator: OperatorOption = {
defaultMessage: 'exists',
}),
value: 'exists',
type: OperatorType.EXISTS,
operator: Operator.INCLUSION,
type: OperatorTypeEnum.EXISTS,
operator: 'included',
};
export const doesNotExistOperator: OperatorOption = {
@ -57,8 +58,8 @@ export const doesNotExistOperator: OperatorOption = {
defaultMessage: 'does not exist',
}),
value: 'does_not_exist',
type: OperatorType.EXISTS,
operator: Operator.EXCLUSION,
type: OperatorTypeEnum.EXISTS,
operator: 'excluded',
};
export const isInListOperator: OperatorOption = {
@ -66,8 +67,8 @@ export const isInListOperator: OperatorOption = {
defaultMessage: 'is in list',
}),
value: 'is_in_list',
type: OperatorType.LIST,
operator: Operator.INCLUSION,
type: OperatorTypeEnum.LIST,
operator: 'included',
};
export const isNotInListOperator: OperatorOption = {
@ -75,8 +76,8 @@ export const isNotInListOperator: OperatorOption = {
defaultMessage: 'is not in list',
}),
value: 'is_not_in_list',
type: OperatorType.LIST,
operator: Operator.EXCLUSION,
type: OperatorTypeEnum.LIST,
operator: 'excluded',
};
export const EXCEPTION_OPERATORS: OperatorOption[] = [

View file

@ -5,11 +5,7 @@
*/
import { ReactNode } from 'react';
import {
NamespaceType,
ExceptionList,
ExceptionListItemSchema as ExceptionItem,
} from '../../../lists_plugin_deps';
import { Operator, OperatorType } from '../../../lists_plugin_deps';
export interface OperatorOption {
message: string;
@ -18,39 +14,13 @@ export interface OperatorOption {
type: OperatorType;
}
export enum Operator {
INCLUSION = 'included',
EXCLUSION = 'excluded',
}
export enum OperatorType {
NESTED = 'nested',
PHRASE = 'match',
PHRASES = 'match_any',
EXISTS = 'exists',
LIST = 'list',
}
export interface FormattedEntry {
fieldName: string;
operator: string | null;
value: string | null;
value: string | string[] | null;
isNested: boolean;
}
export interface NestedExceptionEntry {
field: string;
type: string;
entries: ExceptionEntry[];
}
export interface ExceptionEntry {
field: string;
type: string;
operator: Operator;
value: string;
}
export interface DescriptionListItem {
title: NonNullable<ReactNode>;
description: NonNullable<ReactNode>;
@ -79,47 +49,9 @@ export interface Filter {
pagination: Partial<ExceptionsPagination>;
}
export interface SetExceptionsProps {
lists: ExceptionList[];
exceptions: ExceptionItem[];
pagination: Pagination;
}
export interface ApiProps {
id: string;
namespaceType: NamespaceType;
}
export interface Pagination {
page: number;
perPage: number;
total: number;
}
export interface ExceptionsPagination {
pageIndex: number;
pageSize: number;
totalItemCount: number;
pageSizeOptions: number[];
}
// TODO: Delete once types are updated
export interface ExceptionListItemSchema {
_tags: string[];
comments: Comment[];
created_at: string;
created_by: string;
description?: string;
entries: Array<ExceptionEntry | NestedExceptionEntry>;
id: string;
item_id: string;
list_id: string;
meta?: unknown;
name: string;
namespace_type: 'single' | 'agnostic';
tags: string[];
tie_breaker_id: string;
type: string;
updated_at: string;
updated_by: string;
}

View file

@ -11,7 +11,8 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import moment from 'moment-timezone';
import { ExceptionDetails } from './exception_details';
import { getExceptionItemMock } from '../../mocks';
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
describe('ExceptionDetails', () => {
beforeEach(() => {
@ -23,7 +24,7 @@ describe('ExceptionDetails', () => {
});
test('it renders no comments button if no comments exist', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = [];
const wrapper = mount(
@ -40,8 +41,8 @@ describe('ExceptionDetails', () => {
});
test('it renders comments button if comments exist', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -58,8 +59,8 @@ describe('ExceptionDetails', () => {
});
test('it renders correct number of comments', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = [getCommentsMock()[0]];
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -76,19 +77,8 @@ describe('ExceptionDetails', () => {
});
test('it renders comments plural if more than one', () => {
const exceptionItem = getExceptionItemMock();
exceptionItem.comments = [
{
created_by: 'user_1',
created_at: '2020-04-23T00:19:13.289Z',
comment: 'Comment goes here',
},
{
created_by: 'user_2',
created_at: '2020-04-23T00:19:13.289Z',
comment: 'Comment goes here',
},
];
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -105,7 +95,8 @@ describe('ExceptionDetails', () => {
});
test('it renders comments show text if "showComments" is false', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -117,12 +108,13 @@ describe('ExceptionDetails', () => {
);
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
'Show (1) Comment'
'Show (2) Comments'
);
});
test('it renders comments hide text if "showComments" is true', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -134,13 +126,14 @@ describe('ExceptionDetails', () => {
);
expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual(
'Hide (1) Comment'
'Hide (2) Comments'
);
});
test('it invokes "onCommentsClick" when comments button clicked', () => {
const mockOnCommentsClick = jest.fn();
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -157,7 +150,7 @@ describe('ExceptionDetails', () => {
});
test('it renders the operating system if one is specified in the exception item', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -169,11 +162,11 @@ describe('ExceptionDetails', () => {
);
expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('OS');
expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('Windows');
expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('Linux');
});
test('it renders the exception item creator', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -191,7 +184,7 @@ describe('ExceptionDetails', () => {
});
test('it renders the exception item creation timestamp', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -207,7 +200,7 @@ describe('ExceptionDetails', () => {
});
test('it renders the description if one is included on the exception item', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionDetails
@ -220,7 +213,7 @@ describe('ExceptionDetails', () => {
expect(wrapper.find('EuiDescriptionListTitle').at(3).text()).toEqual('Comment');
expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual(
'This is a description'
'This is a sample endpoint type exception'
);
});
});

View file

@ -15,9 +15,10 @@ import {
import React, { useMemo, Fragment } from 'react';
import styled, { css } from 'styled-components';
import { DescriptionListItem, ExceptionListItemSchema } from '../../types';
import { DescriptionListItem } from '../../types';
import { getDescriptionListContent } from '../../helpers';
import * as i18n from '../../translations';
import { ExceptionListItemSchema } from '../../../../../../public/lists_plugin_deps';
const MyExceptionDetails = styled(EuiFlexItem)`
${({ theme }) => css`

View file

@ -10,7 +10,7 @@ import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionEntries } from './exception_entries';
import { getFormattedEntryMock } from '../../mocks';
import { getFormattedEntryMock } from '../../exceptions.mock';
import { getEmptyValue } from '../../../empty_value';
describe('ExceptionEntries', () => {

View file

@ -12,6 +12,7 @@ import {
EuiButton,
EuiTableFieldDataColumnType,
EuiHideFor,
EuiBadge,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import styled, { css } from 'styled-components';
@ -44,6 +45,7 @@ const MyRemoveButton = styled(EuiButton)`
const MyAndOrBadgeContainer = styled(EuiFlexItem)`
padding-top: ${({ theme }) => theme.eui.euiSizeXL};
padding-bottom: ${({ theme }) => theme.eui.euiSizeS};
`;
interface ExceptionEntriesComponentProps {
@ -101,9 +103,13 @@ const ExceptionEntriesComponent = ({
render: (values: string | string[] | null) => {
if (Array.isArray(values)) {
return (
<EuiFlexGroup direction="row">
<EuiFlexGroup gutterSize="xs" direction="row" justifyContent="flexStart">
{values.map((value) => {
return <EuiFlexItem grow={1}>{value}</EuiFlexItem>;
return (
<EuiFlexItem key={value} grow={false}>
<EuiBadge color="#DDD">{value}</EuiBadge>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);

View file

@ -10,8 +10,8 @@ import { ThemeProvider } from 'styled-components';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionItem } from './';
import { Operator } from '../../types';
import { getExceptionItemMock } from '../../mocks';
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
addDecorator((storyFn) => (
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
@ -19,14 +19,14 @@ addDecorator((storyFn) => (
storiesOf('Components|ExceptionItem', module)
.add('with os', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload.description = '';
payload.comments = [];
payload.entries = [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.INCLUSION,
operator: 'included',
value: 'Elastic, N.V.',
},
];
@ -42,14 +42,14 @@ storiesOf('Components|ExceptionItem', module)
);
})
.add('with description', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.comments = [];
payload.entries = [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.INCLUSION,
operator: 'included',
value: 'Elastic, N.V.',
},
];
@ -65,14 +65,15 @@ storiesOf('Components|ExceptionItem', module)
);
})
.add('with comments', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = '';
payload.comments = getCommentsMock();
payload.entries = [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: Operator.INCLUSION,
operator: 'included',
value: 'Elastic, N.V.',
},
];
@ -88,7 +89,7 @@ storiesOf('Components|ExceptionItem', module)
);
})
.add('with nested entries', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = '';
payload.comments = [];
@ -104,8 +105,8 @@ storiesOf('Components|ExceptionItem', module)
);
})
.add('with everything', () => {
const payload = getExceptionItemMock();
const payload = getExceptionListItemSchemaMock();
payload.comments = getCommentsMock();
return (
<ExceptionItem
loadingItemIds={[]}
@ -117,7 +118,7 @@ storiesOf('Components|ExceptionItem', module)
);
})
.add('with loadingItemIds', () => {
const { id, namespace_type, ...rest } = getExceptionItemMock();
const { id, namespace_type, ...rest } = getExceptionListItemSchemaMock();
return (
<ExceptionItem

View file

@ -10,11 +10,12 @@ import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionItem } from './';
import { getExceptionItemMock } from '../../mocks';
import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
describe('ExceptionItem', () => {
it('it renders ExceptionDetails and ExceptionEntries', () => {
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
@ -34,7 +35,7 @@ describe('ExceptionItem', () => {
it('it invokes "onEditException" when edit button clicked', () => {
const mockOnEditException = jest.fn();
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
@ -51,12 +52,12 @@ describe('ExceptionItem', () => {
const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0);
editBtn.simulate('click');
expect(mockOnEditException).toHaveBeenCalledWith(getExceptionItemMock());
expect(mockOnEditException).toHaveBeenCalledWith(getExceptionListItemSchemaMock());
});
it('it invokes "onDeleteException" when delete button clicked', () => {
const mockOnDeleteException = jest.fn();
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
@ -74,15 +75,15 @@ describe('ExceptionItem', () => {
editBtn.simulate('click');
expect(mockOnDeleteException).toHaveBeenCalledWith({
id: 'uuid_here',
id: '1',
namespaceType: 'single',
});
});
it('it renders comment accordion closed to begin with', () => {
const mockOnDeleteException = jest.fn();
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionItem
@ -100,8 +101,8 @@ describe('ExceptionItem', () => {
it('it renders comment accordion open when showComments is true', () => {
const mockOnDeleteException = jest.fn();
const exceptionItem = getExceptionItemMock();
const exceptionItem = getExceptionListItemSchemaMock();
exceptionItem.comments = getCommentsMock();
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<ExceptionItem

View file

@ -18,7 +18,11 @@ import styled from 'styled-components';
import { ExceptionDetails } from './exception_details';
import { ExceptionEntries } from './exception_entries';
import { getFormattedEntries, getFormattedComments } from '../../helpers';
import { FormattedEntry, ExceptionListItemSchema, ApiProps } from '../../types';
import { FormattedEntry } from '../../types';
import {
ExceptionIdentifiers,
ExceptionListItemSchema,
} from '../../../../../../public/lists_plugin_deps';
const MyFlexItem = styled(EuiFlexItem)`
&.comments--show {
@ -28,10 +32,10 @@ const MyFlexItem = styled(EuiFlexItem)`
`;
interface ExceptionItemProps {
loadingItemIds: ApiProps[];
loadingItemIds: ExceptionIdentifiers[];
exceptionItem: ExceptionListItemSchema;
commentsAccordionId: string;
onDeleteException: (arg: ApiProps) => void;
onDeleteException: (arg: ExceptionIdentifiers) => void;
onEditException: (item: ExceptionListItemSchema) => void;
}

View file

@ -9,7 +9,7 @@ import { ThemeProvider } from 'styled-components';
import { mount } from 'enzyme';
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { getExceptionItemMock } from '../mocks';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { ExceptionsViewerItems } from './exceptions_viewer_items';
describe('ExceptionsViewerItems', () => {
@ -38,7 +38,7 @@ describe('ExceptionsViewerItems', () => {
<ExceptionsViewerItems
showEmpty={false}
isInitLoading={false}
exceptions={[getExceptionItemMock()]}
exceptions={[getExceptionListItemSchemaMock()]}
loadingItemIds={[]}
commentsAccordionId="comments-accordion-id"
onDeleteException={jest.fn()}
@ -71,8 +71,8 @@ describe('ExceptionsViewerItems', () => {
});
it('it does not render or badge for first exception displayed', () => {
const exception1 = getExceptionItemMock();
const exception2 = getExceptionItemMock();
const exception1 = getExceptionListItemSchemaMock();
const exception2 = getExceptionListItemSchemaMock();
exception2.id = 'newId';
const wrapper = mount(
@ -95,8 +95,8 @@ describe('ExceptionsViewerItems', () => {
});
it('it does render or badge with exception displayed', () => {
const exception1 = getExceptionItemMock();
const exception2 = getExceptionItemMock();
const exception1 = getExceptionListItemSchemaMock();
const exception2 = getExceptionListItemSchemaMock();
exception2.id = 'newId';
const wrapper = mount(
@ -128,7 +128,7 @@ describe('ExceptionsViewerItems', () => {
<ExceptionsViewerItems
showEmpty={false}
isInitLoading={false}
exceptions={[getExceptionItemMock()]}
exceptions={[getExceptionListItemSchemaMock()]}
loadingItemIds={[]}
commentsAccordionId="comments-accordion-id"
onDeleteException={mockOnDeleteException}
@ -140,7 +140,7 @@ describe('ExceptionsViewerItems', () => {
wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0).simulate('click');
expect(mockOnDeleteException).toHaveBeenCalledWith({
id: 'uuid_here',
id: '1',
namespaceType: 'single',
});
});

View file

@ -9,9 +9,12 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/e
import styled from 'styled-components';
import * as i18n from '../translations';
import { ExceptionListItemSchema, ApiProps } from '../types';
import { ExceptionItem } from './exception_item';
import { AndOrBadge } from '../../and_or_badge';
import {
ExceptionIdentifiers,
ExceptionListItemSchema,
} from '../../../../../public/lists_plugin_deps';
const MyFlexItem = styled(EuiFlexItem)`
margin: ${({ theme }) => `${theme.eui.euiSize} 0`};
@ -34,9 +37,9 @@ interface ExceptionsViewerItemsProps {
showEmpty: boolean;
isInitLoading: boolean;
exceptions: ExceptionListItemSchema[];
loadingItemIds: ApiProps[];
loadingItemIds: ExceptionIdentifiers[];
commentsAccordionId: string;
onDeleteException: (arg: ApiProps) => void;
onDeleteException: (arg: ExceptionIdentifiers) => void;
onEditExceptionItem: (item: ExceptionListItemSchema) => void;
}

View file

@ -13,7 +13,7 @@ import { ExceptionsViewer } from './';
import { ExceptionListType } from '../types';
import { useKibana } from '../../../../common/lib/kibana';
import { useExceptionList, useApi } from '../../../../../public/lists_plugin_deps';
import { getExceptionListMock } from '../mocks';
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../../public/lists_plugin_deps');
@ -96,7 +96,7 @@ describe('ExceptionsViewer', () => {
it('it renders empty prompt if no exception items exist', () => {
(useExceptionList as jest.Mock).mockReturnValue([
false,
[getExceptionListMock()],
[getExceptionListSchemaMock()],
[],
{
page: 1,

View file

@ -14,17 +14,13 @@ import { useKibana } from '../../../../common/lib/kibana';
import { Panel } from '../../../../common/components/panel';
import { Loader } from '../../../../common/components/loader';
import { ExceptionsViewerHeader } from './exceptions_viewer_header';
import {
ExceptionListType,
ExceptionListItemSchema,
ApiProps,
Filter,
SetExceptionsProps,
} from '../types';
import { ExceptionListType, Filter } from '../types';
import { allExceptionItemsReducer, State } from './reducer';
import {
useExceptionList,
ExceptionIdentifiers,
ExceptionListItemSchema,
UseExceptionListSuccess,
useApi,
} from '../../../../../public/lists_plugin_deps';
import { ExceptionsViewerPagination } from './exceptions_pagination';
@ -107,11 +103,11 @@ const ExceptionsViewerComponent = ({
lists: newLists,
exceptions: newExceptions,
pagination: newPagination,
}: SetExceptionsProps) => {
}: UseExceptionListSuccess) => {
dispatch({
type: 'setExceptions',
lists: newLists,
exceptions: (newExceptions as unknown) as ExceptionListItemSchema[],
exceptions: newExceptions,
pagination: newPagination,
});
},
@ -199,7 +195,7 @@ const ExceptionsViewerComponent = ({
);
const setLoadingItemIds = useCallback(
(items: ApiProps[]): void => {
(items: ExceptionIdentifiers[]): void => {
dispatch({
type: 'updateLoadingItemIds',
items,
@ -209,7 +205,7 @@ const ExceptionsViewerComponent = ({
);
const handleDeleteException = useCallback(
({ id, namespaceType }: ApiProps) => {
({ id, namespaceType }: ExceptionIdentifiers) => {
setLoadingItemIds([{ id, namespaceType }]);
deleteExceptionItem({

View file

@ -3,14 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FilterOptions, ExceptionsPagination } from '../types';
import {
ApiProps,
FilterOptions,
ExceptionsPagination,
ExceptionList,
ExceptionListItemSchema,
ExceptionIdentifiers,
Pagination,
} from '../types';
import { ExceptionList, ExceptionIdentifiers } from '../../../../../public/lists_plugin_deps';
} from '../../../../../public/lists_plugin_deps';
export interface State {
filterOptions: FilterOptions;
@ -21,7 +20,7 @@ export interface State {
exceptions: ExceptionListItemSchema[];
exceptionToEdit: ExceptionListItemSchema | null;
loadingLists: ExceptionIdentifiers[];
loadingItemIds: ApiProps[];
loadingItemIds: ExceptionIdentifiers[];
isInitLoading: boolean;
isModalOpen: boolean;
}
@ -42,7 +41,7 @@ export type Action =
| { type: 'updateIsInitLoading'; loading: boolean }
| { type: 'updateModalOpen'; isOpen: boolean }
| { type: 'updateExceptionToEdit'; exception: ExceptionListItemSchema }
| { type: 'updateLoadingItemIds'; items: ApiProps[] };
| { type: 'updateLoadingItemIds'; items: ExceptionIdentifiers[] };
export const allExceptionItemsReducer = () => (state: State, action: Action): State => {
switch (action.type) {
@ -62,7 +61,7 @@ export const allExceptionItemsReducer = () => (state: State, action: Action): St
...state.pagination,
pageIndex: action.pagination.page - 1,
pageSize: action.pagination.perPage,
totalItemCount: action.pagination.total,
totalItemCount: action.pagination.total ?? 0,
},
allExceptions: action.exceptions,
exceptions: action.exceptions,

View file

@ -11,12 +11,20 @@ export {
usePersistExceptionList,
ExceptionIdentifiers,
ExceptionList,
mockNewExceptionItem,
mockNewExceptionList,
Pagination,
UseExceptionListSuccess,
} from '../../lists/public';
export {
ExceptionListSchema,
ExceptionListItemSchema,
Entries,
Entry,
EntryExists,
EntryNested,
EntriesArray,
NamespaceType,
Operator,
OperatorType,
OperatorTypeEnum,
entriesNested,
entriesExists,
} from '../../lists/common/schemas';