Skip to content

Commit

Permalink
[SIEM][Exceptions] - Updates exception structure and corresponding UI…
Browse files Browse the repository at this point in the history
… types (#69120)

### 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`
  • Loading branch information
yctercero authored Jun 18, 2020
1 parent 38a88e1 commit 2544daf
Show file tree
Hide file tree
Showing 67 changed files with 3,047 additions and 816 deletions.
22 changes: 20 additions & 2 deletions x-pack/plugins/lists/common/constants.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = [];
Expand Down
63 changes: 63 additions & 0 deletions x-pack/plugins/lists/common/schemas/common/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
18 changes: 18 additions & 0 deletions x-pack/plugins/lists/common/schemas/common/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"', () => {
Expand Down Expand Up @@ -75,28 +74,28 @@ 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;
outputPayload.comments = [];
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);
});
Expand All @@ -109,46 +108,46 @@ 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;
outputPayload.namespace_type = 'single';
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;
outputPayload.tags = [];
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;
outputPayload._tags = [];
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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,51 +16,54 @@ 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;
outputPayload.tags = [];
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;
outputPayload._tags = [];
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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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({});
});
});
Loading

0 comments on commit 2544daf

Please sign in to comment.