[Security solution][Exceptions] Add separate io-ts types for endpoint exceptions (#74468)
* Add separate io-ts types for endpoint exception entries * Fix text typos * Fix test * address review comments * Add extra entry validation when adding items to endpoint_list * fix test * fix tests again * really actually fix the tests Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
83678e1388
commit
24773f3a00
|
@ -6,6 +6,7 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import { EntriesArray } from './schemas/types';
|
import { EntriesArray } from './schemas/types';
|
||||||
|
import { EndpointEntriesArray } from './schemas/types/endpoint';
|
||||||
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
|
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
|
||||||
export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z';
|
export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z';
|
||||||
export const USER = 'some user';
|
export const USER = 'some user';
|
||||||
|
@ -41,6 +42,7 @@ export const ITEM_ID = 'some-list-item-id';
|
||||||
export const ENDPOINT_TYPE = 'endpoint';
|
export const ENDPOINT_TYPE = 'endpoint';
|
||||||
export const FIELD = 'host.name';
|
export const FIELD = 'host.name';
|
||||||
export const OPERATOR = 'included';
|
export const OPERATOR = 'included';
|
||||||
|
export const OPERATOR_EXCLUDED = 'excluded';
|
||||||
export const ENTRY_VALUE = 'some host name';
|
export const ENTRY_VALUE = 'some host name';
|
||||||
export const MATCH = 'match';
|
export const MATCH = 'match';
|
||||||
export const MATCH_ANY = 'match_any';
|
export const MATCH_ANY = 'match_any';
|
||||||
|
@ -57,6 +59,14 @@ export const ENTRIES: EntriesArray = [
|
||||||
},
|
},
|
||||||
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
|
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
|
||||||
];
|
];
|
||||||
|
export const ENDPOINT_ENTRIES: EndpointEntriesArray = [
|
||||||
|
{
|
||||||
|
entries: [{ field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }],
|
||||||
|
field: 'some.parentField',
|
||||||
|
type: 'nested',
|
||||||
|
},
|
||||||
|
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
|
||||||
|
];
|
||||||
export const ITEM_TYPE = 'simple';
|
export const ITEM_TYPE = 'simple';
|
||||||
export const _TAGS = [];
|
export const _TAGS = [];
|
||||||
export const TAGS = [];
|
export const TAGS = [];
|
||||||
|
|
|
@ -274,6 +274,7 @@ export type CursorOrUndefined = t.TypeOf<typeof cursorOrUndefined>;
|
||||||
|
|
||||||
export const namespace_type = DefaultNamespace;
|
export const namespace_type = DefaultNamespace;
|
||||||
|
|
||||||
|
export const operatorIncluded = t.keyof({ included: null });
|
||||||
export const operator = t.keyof({ excluded: null, included: null });
|
export const operator = t.keyof({ excluded: null, included: null });
|
||||||
export type Operator = t.TypeOf<typeof operator>;
|
export type Operator = t.TypeOf<typeof operator>;
|
||||||
export enum OperatorEnum {
|
export enum OperatorEnum {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import {
|
import {
|
||||||
COMMENTS,
|
COMMENTS,
|
||||||
DESCRIPTION,
|
DESCRIPTION,
|
||||||
ENTRIES,
|
ENDPOINT_ENTRIES,
|
||||||
ITEM_TYPE,
|
ITEM_TYPE,
|
||||||
META,
|
META,
|
||||||
NAME,
|
NAME,
|
||||||
|
@ -21,7 +21,7 @@ export const getCreateEndpointListItemSchemaMock = (): CreateEndpointListItemSch
|
||||||
_tags: _TAGS,
|
_tags: _TAGS,
|
||||||
comments: COMMENTS,
|
comments: COMMENTS,
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
entries: ENTRIES,
|
entries: ENDPOINT_ENTRIES,
|
||||||
item_id: undefined,
|
item_id: undefined,
|
||||||
meta: META,
|
meta: META,
|
||||||
name: NAME,
|
name: NAME,
|
||||||
|
|
|
@ -18,7 +18,8 @@ import {
|
||||||
tags,
|
tags,
|
||||||
} from '../common/schemas';
|
} from '../common/schemas';
|
||||||
import { RequiredKeepUndefined } from '../../types';
|
import { RequiredKeepUndefined } from '../../types';
|
||||||
import { CreateCommentsArray, DefaultCreateCommentsArray, nonEmptyEntriesArray } from '../types';
|
import { CreateCommentsArray, DefaultCreateCommentsArray } from '../types';
|
||||||
|
import { nonEmptyEndpointEntriesArray } from '../types/endpoint';
|
||||||
import { EntriesArray } from '../types/entries';
|
import { EntriesArray } from '../types/entries';
|
||||||
import { DefaultUuid } from '../../shared_imports';
|
import { DefaultUuid } from '../../shared_imports';
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ export const createEndpointListItemSchema = t.intersection([
|
||||||
t.exact(
|
t.exact(
|
||||||
t.type({
|
t.type({
|
||||||
description,
|
description,
|
||||||
entries: nonEmptyEntriesArray,
|
entries: nonEmptyEndpointEntriesArray,
|
||||||
name,
|
name,
|
||||||
type: exceptionListItemType,
|
type: exceptionListItemType,
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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 { EndpointEntriesArray } from './entries';
|
||||||
|
import { getEndpointEntryMatchMock } from './entry_match.mock';
|
||||||
|
import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock';
|
||||||
|
import { getEndpointEntryNestedMock } from './entry_nested.mock';
|
||||||
|
|
||||||
|
export const getEndpointEntriesArrayMock = (): EndpointEntriesArray => [
|
||||||
|
getEndpointEntryMatchMock(),
|
||||||
|
getEndpointEntryMatchAnyMock(),
|
||||||
|
getEndpointEntryNestedMock(),
|
||||||
|
];
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* 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 '../../../shared_imports';
|
||||||
|
import { getEntryExistsMock } from '../entry_exists.mock';
|
||||||
|
import { getEntryListMock } from '../entry_list.mock';
|
||||||
|
|
||||||
|
import { getEndpointEntryMatchMock } from './entry_match.mock';
|
||||||
|
import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock';
|
||||||
|
import { getEndpointEntryNestedMock } from './entry_nested.mock';
|
||||||
|
import { getEndpointEntriesArrayMock } from './entries.mock';
|
||||||
|
import {
|
||||||
|
NonEmptyEndpointEntriesArray,
|
||||||
|
endpointEntriesArray,
|
||||||
|
nonEmptyEndpointEntriesArray,
|
||||||
|
} from './entries';
|
||||||
|
|
||||||
|
describe('Endpoint', () => {
|
||||||
|
describe('entriesArray', () => {
|
||||||
|
test('it should validate an array with match entry', () => {
|
||||||
|
const payload = [getEndpointEntryMatchMock()];
|
||||||
|
const decoded = endpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should validate an array with match_any entry', () => {
|
||||||
|
const payload = [getEndpointEntryMatchAnyMock()];
|
||||||
|
const decoded = endpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should NOT validate an empty array', () => {
|
||||||
|
const payload: NonEmptyEndpointEntriesArray = [];
|
||||||
|
const decoded = nonEmptyEndpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([
|
||||||
|
'Invalid value "[]" supplied to "NonEmptyEndpointEntriesArray"',
|
||||||
|
]);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => {
|
||||||
|
const payload: NonEmptyEndpointEntriesArray = [getEndpointEntryMatchAnyMock()];
|
||||||
|
const guarded = nonEmptyEndpointEntriesArray.is(payload);
|
||||||
|
expect(guarded).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => {
|
||||||
|
const payload: NonEmptyEndpointEntriesArray = [];
|
||||||
|
const guarded = nonEmptyEndpointEntriesArray.is(payload);
|
||||||
|
expect(guarded).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should NOT validate an array with exists entry', () => {
|
||||||
|
const payload = [getEntryExistsMock()];
|
||||||
|
const decoded = endpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([
|
||||||
|
'Invalid value "exists" supplied to "type"',
|
||||||
|
'Invalid value "undefined" supplied to "value"',
|
||||||
|
'Invalid value "undefined" supplied to "entries"',
|
||||||
|
]);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should NOT validate an array with list entry', () => {
|
||||||
|
const payload = [getEntryListMock()];
|
||||||
|
const decoded = endpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([
|
||||||
|
'Invalid value "list" supplied to "type"',
|
||||||
|
'Invalid value "undefined" supplied to "value"',
|
||||||
|
'Invalid value "undefined" supplied to "entries"',
|
||||||
|
]);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should validate an array with nested entry', () => {
|
||||||
|
const payload = [getEndpointEntryNestedMock()];
|
||||||
|
const decoded = endpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should validate an array with all types of entries', () => {
|
||||||
|
const payload = getEndpointEntriesArrayMock();
|
||||||
|
const decoded = endpointEntriesArray.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
import { Either } from 'fp-ts/lib/Either';
|
||||||
|
|
||||||
|
import { endpointEntryMatchAny } from './entry_match_any';
|
||||||
|
import { endpointEntryMatch } from './entry_match';
|
||||||
|
import { endpointEntryNested } from './entry_nested';
|
||||||
|
|
||||||
|
export const endpointEntriesArray = t.array(
|
||||||
|
t.union([endpointEntryMatch, endpointEntryMatchAny, endpointEntryNested])
|
||||||
|
);
|
||||||
|
export type EndpointEntriesArray = t.TypeOf<typeof endpointEntriesArray>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types the nonEmptyEndpointEntriesArray as:
|
||||||
|
* - An array of entries of length 1 or greater
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const nonEmptyEndpointEntriesArray = new t.Type<
|
||||||
|
EndpointEntriesArray,
|
||||||
|
EndpointEntriesArray,
|
||||||
|
unknown
|
||||||
|
>(
|
||||||
|
'NonEmptyEndpointEntriesArray',
|
||||||
|
(u: unknown): u is EndpointEntriesArray => endpointEntriesArray.is(u) && u.length > 0,
|
||||||
|
(input, context): Either<t.Errors, EndpointEntriesArray> => {
|
||||||
|
if (Array.isArray(input) && input.length === 0) {
|
||||||
|
return t.failure(input, context);
|
||||||
|
} else {
|
||||||
|
return endpointEntriesArray.validate(input, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t.identity
|
||||||
|
);
|
||||||
|
|
||||||
|
export type NonEmptyEndpointEntriesArray = t.OutputOf<typeof nonEmptyEndpointEntriesArray>;
|
||||||
|
export type NonEmptyEndpointEntriesArrayDecoded = t.TypeOf<typeof nonEmptyEndpointEntriesArray>;
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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, FIELD, MATCH, OPERATOR } from '../../../constants.mock';
|
||||||
|
|
||||||
|
import { EndpointEntryMatch } from './entry_match';
|
||||||
|
|
||||||
|
export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({
|
||||||
|
field: FIELD,
|
||||||
|
operator: OPERATOR,
|
||||||
|
type: MATCH,
|
||||||
|
value: ENTRY_VALUE,
|
||||||
|
});
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* 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 '../../../shared_imports';
|
||||||
|
import { getEntryMatchMock } from '../entry_match.mock';
|
||||||
|
|
||||||
|
import { getEndpointEntryMatchMock } from './entry_match.mock';
|
||||||
|
import { EndpointEntryMatch, endpointEntryMatch } from './entry_match';
|
||||||
|
|
||||||
|
describe('endpointEntryMatch', () => {
|
||||||
|
test('it should validate an entry', () => {
|
||||||
|
const payload = getEndpointEntryMatchMock();
|
||||||
|
const decoded = endpointEntryMatch.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should NOT validate when "operator" is "excluded"', () => {
|
||||||
|
// Use the generic entry mock so we can test operator: excluded
|
||||||
|
const payload = getEntryMatchMock();
|
||||||
|
payload.operator = 'excluded';
|
||||||
|
const decoded = endpointEntryMatch.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([
|
||||||
|
'Invalid value "excluded" supplied to "operator"',
|
||||||
|
]);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when "field" is empty string', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatch, 'field'> & { field: string } = {
|
||||||
|
...getEndpointEntryMatchMock(),
|
||||||
|
field: '',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatch.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when "value" is not string', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatch, 'value'> & { value: string[] } = {
|
||||||
|
...getEndpointEntryMatchMock(),
|
||||||
|
value: ['some value'],
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatch.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 FAIL validation when "value" is empty string', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatch, 'value'> & { value: string } = {
|
||||||
|
...getEndpointEntryMatchMock(),
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatch.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when "type" is not "match"', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatch, 'type'> & { type: string } = {
|
||||||
|
...getEndpointEntryMatchMock(),
|
||||||
|
type: 'match_any',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatch.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: EndpointEntryMatch & {
|
||||||
|
extraKey?: string;
|
||||||
|
} = getEndpointEntryMatchMock();
|
||||||
|
payload.extraKey = 'some value';
|
||||||
|
const decoded = endpointEntryMatch.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(getEntryMatchMock());
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
|
import { NonEmptyString } from '../../../shared_imports';
|
||||||
|
import { operatorIncluded } from '../../common/schemas';
|
||||||
|
|
||||||
|
export const endpointEntryMatch = t.exact(
|
||||||
|
t.type({
|
||||||
|
field: NonEmptyString,
|
||||||
|
operator: operatorIncluded,
|
||||||
|
type: t.keyof({ match: null }),
|
||||||
|
value: NonEmptyString,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export type EndpointEntryMatch = t.TypeOf<typeof endpointEntryMatch>;
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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, FIELD, MATCH_ANY, OPERATOR } from '../../../constants.mock';
|
||||||
|
|
||||||
|
import { EndpointEntryMatchAny } from './entry_match_any';
|
||||||
|
|
||||||
|
export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({
|
||||||
|
field: FIELD,
|
||||||
|
operator: OPERATOR,
|
||||||
|
type: MATCH_ANY,
|
||||||
|
value: [ENTRY_VALUE],
|
||||||
|
});
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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 '../../../shared_imports';
|
||||||
|
import { getEntryMatchAnyMock } from '../entry_match_any.mock';
|
||||||
|
|
||||||
|
import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock';
|
||||||
|
import { EndpointEntryMatchAny, endpointEntryMatchAny } from './entry_match_any';
|
||||||
|
|
||||||
|
describe('endpointEntryMatchAny', () => {
|
||||||
|
test('it should validate an entry', () => {
|
||||||
|
const payload = getEndpointEntryMatchAnyMock();
|
||||||
|
const decoded = endpointEntryMatchAny.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should NOT validate when operator is "excluded"', () => {
|
||||||
|
// Use the generic entry mock so we can test operator: excluded
|
||||||
|
const payload = getEntryMatchAnyMock();
|
||||||
|
payload.operator = 'excluded';
|
||||||
|
const decoded = endpointEntryMatchAny.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([
|
||||||
|
'Invalid value "excluded" supplied to "operator"',
|
||||||
|
]);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when field is empty string', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatchAny, 'field'> & { field: string } = {
|
||||||
|
...getEndpointEntryMatchAnyMock(),
|
||||||
|
field: '',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatchAny.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when value is empty array', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatchAny, 'value'> & { value: string[] } = {
|
||||||
|
...getEndpointEntryMatchAnyMock(),
|
||||||
|
value: [],
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatchAny.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when value is not string array', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatchAny, 'value'> & { value: string } = {
|
||||||
|
...getEndpointEntryMatchAnyMock(),
|
||||||
|
value: 'some string',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatchAny.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 FAIL validation when "type" is not "match_any"', () => {
|
||||||
|
const payload: Omit<EndpointEntryMatchAny, 'type'> & { type: string } = {
|
||||||
|
...getEndpointEntryMatchAnyMock(),
|
||||||
|
type: 'match',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryMatchAny.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: EndpointEntryMatchAny & {
|
||||||
|
extraKey?: string;
|
||||||
|
} = getEndpointEntryMatchAnyMock();
|
||||||
|
payload.extraKey = 'some extra key';
|
||||||
|
const decoded = endpointEntryMatchAny.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(getEntryMatchAnyMock());
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 * as t from 'io-ts';
|
||||||
|
|
||||||
|
import { NonEmptyString } from '../../../shared_imports';
|
||||||
|
import { operatorIncluded } from '../../common/schemas';
|
||||||
|
import { nonEmptyOrNullableStringArray } from '../non_empty_or_nullable_string_array';
|
||||||
|
|
||||||
|
export const endpointEntryMatchAny = t.exact(
|
||||||
|
t.type({
|
||||||
|
field: NonEmptyString,
|
||||||
|
operator: operatorIncluded,
|
||||||
|
type: t.keyof({ match_any: null }),
|
||||||
|
value: nonEmptyOrNullableStringArray,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export type EndpointEntryMatchAny = t.TypeOf<typeof endpointEntryMatchAny>;
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* 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 { FIELD, NESTED } from '../../../constants.mock';
|
||||||
|
|
||||||
|
import { EndpointEntryNested } from './entry_nested';
|
||||||
|
import { getEndpointEntryMatchMock } from './entry_match.mock';
|
||||||
|
import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock';
|
||||||
|
|
||||||
|
export const getEndpointEntryNestedMock = (): EndpointEntryNested => ({
|
||||||
|
entries: [getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock()],
|
||||||
|
field: FIELD,
|
||||||
|
type: NESTED,
|
||||||
|
});
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* 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 '../../../shared_imports';
|
||||||
|
import { getEntryExistsMock } from '../entry_exists.mock';
|
||||||
|
|
||||||
|
import { getEndpointEntryNestedMock } from './entry_nested.mock';
|
||||||
|
import { EndpointEntryNested, endpointEntryNested } from './entry_nested';
|
||||||
|
import { getEndpointEntryMatchAnyMock } from './entry_match_any.mock';
|
||||||
|
import {
|
||||||
|
NonEmptyEndpointNestedEntriesArray,
|
||||||
|
nonEmptyEndpointNestedEntriesArray,
|
||||||
|
} from './non_empty_nested_entries_array';
|
||||||
|
import { getEndpointEntryMatchMock } from './entry_match.mock';
|
||||||
|
|
||||||
|
describe('endpointEntryNested', () => {
|
||||||
|
test('it should validate a nested entry', () => {
|
||||||
|
const payload = getEndpointEntryNestedMock();
|
||||||
|
const decoded = endpointEntryNested.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when "type" is not "nested"', () => {
|
||||||
|
const payload: Omit<EndpointEntryNested, 'type'> & { type: 'match' } = {
|
||||||
|
...getEndpointEntryNestedMock(),
|
||||||
|
type: 'match',
|
||||||
|
};
|
||||||
|
const decoded = endpointEntryNested.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 FAIL validation when "field" is empty string', () => {
|
||||||
|
const payload: Omit<EndpointEntryNested, 'field'> & {
|
||||||
|
field: string;
|
||||||
|
} = { ...getEndpointEntryNestedMock(), field: '' };
|
||||||
|
const decoded = endpointEntryNested.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should FAIL validation when "field" is not a string', () => {
|
||||||
|
const payload: Omit<EndpointEntryNested, 'field'> & {
|
||||||
|
field: number;
|
||||||
|
} = { ...getEndpointEntryNestedMock(), field: 1 };
|
||||||
|
const decoded = endpointEntryNested.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 FAIL validation when "entries" is not an array', () => {
|
||||||
|
const payload: Omit<EndpointEntryNested, 'entries'> & {
|
||||||
|
entries: string;
|
||||||
|
} = { ...getEndpointEntryNestedMock(), entries: 'im a string' };
|
||||||
|
const decoded = endpointEntryNested.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 validate when "entries" contains an entry item that is type "match"', () => {
|
||||||
|
const payload = { ...getEndpointEntryNestedMock(), entries: [getEndpointEntryMatchAnyMock()] };
|
||||||
|
const decoded = endpointEntryNested.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual({
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
field: 'host.name',
|
||||||
|
operator: 'included',
|
||||||
|
type: 'match_any',
|
||||||
|
value: ['some host name'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
field: 'host.name',
|
||||||
|
type: 'nested',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should NOT validate when "entries" contains an entry item that is type "exists"', () => {
|
||||||
|
const payload = { ...getEndpointEntryNestedMock(), entries: [getEntryExistsMock()] };
|
||||||
|
const decoded = endpointEntryNested.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([
|
||||||
|
'Invalid value "exists" supplied to "entries,type"',
|
||||||
|
'Invalid value "undefined" supplied to "entries,value"',
|
||||||
|
]);
|
||||||
|
expect(message.schema).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should strip out extra keys', () => {
|
||||||
|
const payload: EndpointEntryNested & {
|
||||||
|
extraKey?: string;
|
||||||
|
} = getEndpointEntryNestedMock();
|
||||||
|
payload.extraKey = 'some extra key';
|
||||||
|
const decoded = endpointEntryNested.decode(payload);
|
||||||
|
const message = pipe(decoded, foldLeftRight);
|
||||||
|
|
||||||
|
expect(getPaths(left(message.errors))).toEqual([]);
|
||||||
|
expect(message.schema).toEqual(getEndpointEntryNestedMock());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => {
|
||||||
|
const payload: NonEmptyEndpointNestedEntriesArray = [
|
||||||
|
getEndpointEntryMatchMock(),
|
||||||
|
getEndpointEntryMatchAnyMock(),
|
||||||
|
];
|
||||||
|
const guarded = nonEmptyEndpointNestedEntriesArray.is(payload);
|
||||||
|
expect(guarded).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => {
|
||||||
|
const payload: NonEmptyEndpointNestedEntriesArray = [];
|
||||||
|
const guarded = nonEmptyEndpointNestedEntriesArray.is(payload);
|
||||||
|
expect(guarded).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
|
import { NonEmptyString } from '../../../shared_imports';
|
||||||
|
|
||||||
|
import { nonEmptyEndpointNestedEntriesArray } from './non_empty_nested_entries_array';
|
||||||
|
|
||||||
|
export const endpointEntryNested = t.exact(
|
||||||
|
t.type({
|
||||||
|
entries: nonEmptyEndpointNestedEntriesArray,
|
||||||
|
field: NonEmptyString,
|
||||||
|
type: t.keyof({ nested: null }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export type EndpointEntryNested = t.TypeOf<typeof endpointEntryNested>;
|
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './entries';
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
import { Either } from 'fp-ts/lib/Either';
|
||||||
|
|
||||||
|
import { endpointEntryMatchAny } from './entry_match_any';
|
||||||
|
import { endpointEntryMatch } from './entry_match';
|
||||||
|
|
||||||
|
export const endpointNestedEntriesArray = t.array(
|
||||||
|
t.union([endpointEntryMatch, endpointEntryMatchAny])
|
||||||
|
);
|
||||||
|
export type EndpointNestedEntriesArray = t.TypeOf<typeof endpointNestedEntriesArray>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types the nonEmptyNestedEntriesArray as:
|
||||||
|
* - An array of entries of length 1 or greater
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const nonEmptyEndpointNestedEntriesArray = new t.Type<
|
||||||
|
EndpointNestedEntriesArray,
|
||||||
|
EndpointNestedEntriesArray,
|
||||||
|
unknown
|
||||||
|
>(
|
||||||
|
'NonEmptyEndpointNestedEntriesArray',
|
||||||
|
(u: unknown): u is EndpointNestedEntriesArray => endpointNestedEntriesArray.is(u) && u.length > 0,
|
||||||
|
(input, context): Either<t.Errors, EndpointNestedEntriesArray> => {
|
||||||
|
if (Array.isArray(input) && input.length === 0) {
|
||||||
|
return t.failure(input, context);
|
||||||
|
} else {
|
||||||
|
return endpointNestedEntriesArray.validate(input, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t.identity
|
||||||
|
);
|
||||||
|
|
||||||
|
export type NonEmptyEndpointNestedEntriesArray = t.OutputOf<
|
||||||
|
typeof nonEmptyEndpointNestedEntriesArray
|
||||||
|
>;
|
||||||
|
export type NonEmptyEndpointNestedEntriesArrayDecoded = t.TypeOf<
|
||||||
|
typeof nonEmptyEndpointNestedEntriesArray
|
||||||
|
>;
|
|
@ -17,7 +17,7 @@ import {
|
||||||
|
|
||||||
import { getExceptionListClient } from './utils/get_exception_list_client';
|
import { getExceptionListClient } from './utils/get_exception_list_client';
|
||||||
import { endpointDisallowedFields } from './endpoint_disallowed_fields';
|
import { endpointDisallowedFields } from './endpoint_disallowed_fields';
|
||||||
import { validateExceptionListSize } from './validate';
|
import { validateEndpointExceptionItemEntries, validateExceptionListSize } from './validate';
|
||||||
|
|
||||||
export const createExceptionListItemRoute = (router: IRouter): void => {
|
export const createExceptionListItemRoute = (router: IRouter): void => {
|
||||||
router.post(
|
router.post(
|
||||||
|
@ -73,13 +73,11 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (exceptionList.type === 'endpoint') {
|
if (exceptionList.type === 'endpoint') {
|
||||||
for (const entry of entries) {
|
const error = validateEndpointExceptionItemEntries(request.body.entries);
|
||||||
if (entry.type === 'list') {
|
if (error != null) {
|
||||||
return siemResponse.error({
|
return siemResponse.error(error);
|
||||||
body: `cannot add exception item with entry of type "list" to endpoint exception list`,
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
if (endpointDisallowedFields.includes(entry.field)) {
|
if (endpointDisallowedFields.includes(entry.field)) {
|
||||||
return siemResponse.error({
|
return siemResponse.error({
|
||||||
body: `cannot add endpoint exception item on field ${entry.field}`,
|
body: `cannot add endpoint exception item on field ${entry.field}`,
|
||||||
|
|
|
@ -4,11 +4,17 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
import { fold } from 'fp-ts/lib/Either';
|
||||||
|
|
||||||
import { ExceptionListClient } from '../services/exception_lists/exception_list_client';
|
import { ExceptionListClient } from '../services/exception_lists/exception_list_client';
|
||||||
import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants';
|
import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants';
|
||||||
import { foundExceptionListItemSchema } from '../../common/schemas';
|
import { foundExceptionListItemSchema } from '../../common/schemas';
|
||||||
import { NamespaceType } from '../../common/schemas/types';
|
import { NamespaceType, NonEmptyEntriesArray } from '../../common/schemas/types';
|
||||||
import { validate } from '../../common/shared_imports';
|
import { nonEmptyEndpointEntriesArray } from '../../common/schemas/types/endpoint';
|
||||||
|
import { exactCheck, validate } from '../../common/shared_imports';
|
||||||
|
import { formatErrors } from '../siem_server_deps';
|
||||||
|
|
||||||
export const validateExceptionListSize = async (
|
export const validateExceptionListSize = async (
|
||||||
exceptionLists: ExceptionListClient,
|
exceptionLists: ExceptionListClient,
|
||||||
|
@ -54,3 +60,20 @@ export const validateExceptionListSize = async (
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateEndpointExceptionItemEntries = (
|
||||||
|
entries: NonEmptyEntriesArray
|
||||||
|
): { body: string[]; statusCode: number } | null =>
|
||||||
|
pipe(
|
||||||
|
nonEmptyEndpointEntriesArray.decode(entries),
|
||||||
|
(decoded) => exactCheck(entries, decoded),
|
||||||
|
fold(
|
||||||
|
(errors: t.Errors) => {
|
||||||
|
return {
|
||||||
|
body: formatErrors(errors),
|
||||||
|
statusCode: 400,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
() => null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
|
@ -8,8 +8,9 @@
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"field": "actingProcess.file.signer",
|
"field": "actingProcess.file.signer",
|
||||||
"operator": "excluded",
|
"operator": "included",
|
||||||
"type": "exists"
|
"type": "match",
|
||||||
|
"value": "test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "host.name",
|
"field": "host.name",
|
||||||
|
|
|
@ -19,3 +19,5 @@ export {
|
||||||
buildRouteValidation,
|
buildRouteValidation,
|
||||||
readPrivileges,
|
readPrivileges,
|
||||||
} from '../../security_solution/server';
|
} from '../../security_solution/server';
|
||||||
|
|
||||||
|
export { formatErrors } from '../../security_solution/common';
|
||||||
|
|
|
@ -40,9 +40,11 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send(badItem)
|
.send(badItem)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
expect(body.message).to.eql(
|
expect(body.message).to.eql([
|
||||||
'cannot add exception item with entry of type "list" to endpoint exception list'
|
'Invalid value "list" supplied to "type"',
|
||||||
);
|
'Invalid value "undefined" supplied to "value"',
|
||||||
|
'Invalid value "undefined" supplied to "entries"',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a 400 if endpoint exception entry has disallowed field', async () => {
|
it('should return a 400 if endpoint exception entry has disallowed field', async () => {
|
||||||
|
|
Loading…
Reference in a new issue