Skip to content

Commit

Permalink
[Lists][Exceptions] - Updates exception list item comments structure (e…
Browse files Browse the repository at this point in the history
…lastic#68864)

### Summary

This is part of a series of upcoming changes to the exception list item structure. This PR focuses solely on updating exception_item.comment. The hope is to keep these PRs relatively small.

- Updates exception_item.comment structure which was previously a string to exception_item.comments which is an array of { comment: string; created_by: string; created_at: string; }
- Adds a few unit tests server side
- Fixes some minor misspellings
- Updates ExceptionViewer component in the UI to account for new structure
  • Loading branch information
yctercero committed Jun 11, 2020
1 parent 118e66f commit d9a1219
Show file tree
Hide file tree
Showing 38 changed files with 866 additions and 90 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/lists/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ And you can attach exception list items like so:
"malware",
"os:linux"
],
"comment": [],
"comments": [],
"created_at": "2020-05-28T19:17:21.099Z",
"created_by": "yo",
"description": "This is a sample endpoint type exception",
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/lists/common/constants.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export const VALUE_2 = '255.255.255';
export const NAMESPACE_TYPE = 'single';

// Exception List specific
export const ID = 'uuid_here';
export const ENDPOINT_TYPE = 'endpoint';
export const ENTRIES = [
{ field: 'some.field', match: 'some value', match_any: undefined, operator: 'included' },
];
export const ITEM_TYPE = 'simple';
export const _TAGS = [];
export const TAGS = [];
export const COMMENT = [];
export const COMMENTS = [];
6 changes: 0 additions & 6 deletions x-pack/plugins/lists/common/schemas/common/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,6 @@ export type ExceptionListItemType = t.TypeOf<typeof exceptionListItemType>;
export const list_type = t.keyof({ item: null, list: null });
export type ListType = t.TypeOf<typeof list_type>;

// TODO: Investigate what the deep structure of a comment is really going to be and then change this to use that deep structure with a default array
export const comment = DefaultStringArray;
export type Comment = t.TypeOf<typeof comment>;
export const commentOrUndefined = t.union([comment, t.undefined]);
export type CommentOrUndefined = t.TypeOf<typeof commentOrUndefined>;

export const item_id = NonEmptyString;
export type ItemId = t.TypeOf<typeof item_id>;
export const itemIdOrUndefined = t.union([item_id, t.undefined]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import {
COMMENT,
COMMENTS,
DESCRIPTION,
ENTRIES,
ITEM_TYPE,
Expand All @@ -21,7 +21,7 @@ import { CreateExceptionListItemSchema } from './create_exception_list_item_sche

export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({
_tags: _TAGS,
comment: COMMENT,
comments: COMMENTS,
description: DESCRIPTION,
entries: ENTRIES,
item_id: undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* 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 {
CreateExceptionListItemSchema,
createExceptionListItemSchema,
} 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', () => {
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;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});

test('it should not accept an undefined for "description"', () => {
const payload = getCreateExceptionListItemSchemaMock();
delete payload.description;
const decoded = createExceptionListItemSchema.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 = getCreateExceptionListItemSchemaMock();
delete payload.name;
const decoded = createExceptionListItemSchema.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 = getCreateExceptionListItemSchemaMock();
delete payload.type;
const decoded = createExceptionListItemSchema.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 "list_id"', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.list_id;
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, 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 accept an undefined for "meta" but strip it out', () => {
const payload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete payload.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;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});

test('it should accept an undefined for "comments" but return an array', () => {
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;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});

test('it should accept an undefined for "entries" but return an array', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.entries;
outputPayload.entries = [];
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
outputPayload.item_id = (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"', () => {
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;
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 = 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;
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 = 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;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});

test('it should accept an undefined for "item_id" and auto generate a uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.item_id;
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as CreateExceptionListItemSchema).item_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 "item_id" and generate a correct body not counting the uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.item_id;
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(message.schema).toEqual(inputPayload);
});

test('it should not allow an extra key to be sent in', () => {
const payload: CreateExceptionListItemSchema & {
extraKey?: string;
} = getCreateExceptionListItemSchemaMock();
payload.extraKey = 'some new value';
const decoded = createExceptionListItemSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
expect(message.schema).toEqual({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
Tags,
_Tags,
_tags,
comment,
description,
exceptionListItemType,
list_id,
Expand All @@ -24,7 +23,7 @@ import {
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
import { DefaultEntryArray } from '../types';
import { CommentsPartialArray, DefaultCommentsPartialArray, DefaultEntryArray } from '../types';
import { EntriesArray } from '../types/entries';
import { DefaultUuid } from '../../siem_common_deps';

Expand All @@ -40,7 +39,7 @@ export const createExceptionListItemSchema = t.intersection([
t.exact(
t.partial({
_tags, // defaults to empty array if not set during decode
comment, // defaults to empty array if not set during decode
comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode
entries: DefaultEntryArray, // defaults to empty array if not set during decode
item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode
meta, // defaults to undefined if not set during decode
Expand All @@ -61,9 +60,10 @@ export type CreateExceptionListItemSchema = RequiredKeepUndefined<
export type CreateExceptionListItemSchemaDecoded = Identity<
Omit<
CreateExceptionListItemSchema,
'_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type'
'_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
> & {
_tags: _Tags;
comments: CommentsPartialArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 {
COMMENTS,
DESCRIPTION,
ENTRIES,
ID,
ITEM_TYPE,
LIST_ITEM_ID,
META,
NAME,
NAMESPACE_TYPE,
TAGS,
_TAGS,
} from '../../constants.mock';

import { UpdateExceptionListItemSchema } from './update_exception_list_item_schema';

export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({
_tags: _TAGS,
comments: COMMENTS,
description: DESCRIPTION,
entries: ENTRIES,
id: ID,
item_id: LIST_ITEM_ID,
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
tags: TAGS,
type: ITEM_TYPE,
});
Loading

0 comments on commit d9a1219

Please sign in to comment.