[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 { EntriesArray } from './schemas/types';
|
||||
import { EndpointEntriesArray } from './schemas/types/endpoint';
|
||||
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 USER = 'some user';
|
||||
|
@ -41,6 +42,7 @@ export const ITEM_ID = 'some-list-item-id';
|
|||
export const ENDPOINT_TYPE = 'endpoint';
|
||||
export const FIELD = 'host.name';
|
||||
export const OPERATOR = 'included';
|
||||
export const OPERATOR_EXCLUDED = 'excluded';
|
||||
export const ENTRY_VALUE = 'some host name';
|
||||
export const MATCH = 'match';
|
||||
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' },
|
||||
];
|
||||
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 _TAGS = [];
|
||||
export const TAGS = [];
|
||||
|
|
|
@ -274,6 +274,7 @@ export type CursorOrUndefined = t.TypeOf<typeof cursorOrUndefined>;
|
|||
|
||||
export const namespace_type = DefaultNamespace;
|
||||
|
||||
export const operatorIncluded = t.keyof({ included: null });
|
||||
export const operator = t.keyof({ excluded: null, included: null });
|
||||
export type Operator = t.TypeOf<typeof operator>;
|
||||
export enum OperatorEnum {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import {
|
||||
COMMENTS,
|
||||
DESCRIPTION,
|
||||
ENTRIES,
|
||||
ENDPOINT_ENTRIES,
|
||||
ITEM_TYPE,
|
||||
META,
|
||||
NAME,
|
||||
|
@ -21,7 +21,7 @@ export const getCreateEndpointListItemSchemaMock = (): CreateEndpointListItemSch
|
|||
_tags: _TAGS,
|
||||
comments: COMMENTS,
|
||||
description: DESCRIPTION,
|
||||
entries: ENTRIES,
|
||||
entries: ENDPOINT_ENTRIES,
|
||||
item_id: undefined,
|
||||
meta: META,
|
||||
name: NAME,
|
||||
|
|
|
@ -18,7 +18,8 @@ import {
|
|||
tags,
|
||||
} from '../common/schemas';
|
||||
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 { DefaultUuid } from '../../shared_imports';
|
||||
|
||||
|
@ -26,7 +27,7 @@ export const createEndpointListItemSchema = t.intersection([
|
|||
t.exact(
|
||||
t.type({
|
||||
description,
|
||||
entries: nonEmptyEntriesArray,
|
||||
entries: nonEmptyEndpointEntriesArray,
|
||||
name,
|
||||
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 { endpointDisallowedFields } from './endpoint_disallowed_fields';
|
||||
import { validateExceptionListSize } from './validate';
|
||||
import { validateEndpointExceptionItemEntries, validateExceptionListSize } from './validate';
|
||||
|
||||
export const createExceptionListItemRoute = (router: IRouter): void => {
|
||||
router.post(
|
||||
|
@ -73,13 +73,11 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
|
|||
});
|
||||
} else {
|
||||
if (exceptionList.type === 'endpoint') {
|
||||
const error = validateEndpointExceptionItemEntries(request.body.entries);
|
||||
if (error != null) {
|
||||
return siemResponse.error(error);
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.type === 'list') {
|
||||
return siemResponse.error({
|
||||
body: `cannot add exception item with entry of type "list" to endpoint exception list`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
if (endpointDisallowedFields.includes(entry.field)) {
|
||||
return siemResponse.error({
|
||||
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.
|
||||
*/
|
||||
|
||||
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 { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants';
|
||||
import { foundExceptionListItemSchema } from '../../common/schemas';
|
||||
import { NamespaceType } from '../../common/schemas/types';
|
||||
import { validate } from '../../common/shared_imports';
|
||||
import { NamespaceType, NonEmptyEntriesArray } from '../../common/schemas/types';
|
||||
import { nonEmptyEndpointEntriesArray } from '../../common/schemas/types/endpoint';
|
||||
import { exactCheck, validate } from '../../common/shared_imports';
|
||||
import { formatErrors } from '../siem_server_deps';
|
||||
|
||||
export const validateExceptionListSize = async (
|
||||
exceptionLists: ExceptionListClient,
|
||||
|
@ -54,3 +60,20 @@ export const validateExceptionListSize = async (
|
|||
}
|
||||
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": [
|
||||
{
|
||||
"field": "actingProcess.file.signer",
|
||||
"operator": "excluded",
|
||||
"type": "exists"
|
||||
"operator": "included",
|
||||
"type": "match",
|
||||
"value": "test"
|
||||
},
|
||||
{
|
||||
"field": "host.name",
|
||||
|
|
|
@ -19,3 +19,5 @@ export {
|
|||
buildRouteValidation,
|
||||
readPrivileges,
|
||||
} from '../../security_solution/server';
|
||||
|
||||
export { formatErrors } from '../../security_solution/common';
|
||||
|
|
|
@ -40,9 +40,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.set('kbn-xsrf', 'xxx')
|
||||
.send(badItem)
|
||||
.expect(400);
|
||||
expect(body.message).to.eql(
|
||||
'cannot add exception item with entry of type "list" to endpoint exception list'
|
||||
);
|
||||
expect(body.message).to.eql([
|
||||
'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 () => {
|
||||
|
|
Loading…
Reference in a new issue