[Security Solution][Exceptions] - Require non empty entries and non empty string values in exception list items (#72748)

## Summary

This PR updates the exception list entries schemas.

- **Prior:** `entries` could be `undefined` or empty array on `ExceptionListItemSchema`
  - **Now:** `entries` is a required field that cannot be empty - there's really no use for an item without `entries`

- **Prior:** `field` and `value` could be empty string in `EntryMatch`
  - **Now:** `field` and `value` can no longer be empty strings

- **Prior:** `field` could be empty string and `value` could be empty array in `EntryMatchAny`
  - **Now:** `field` and `value` can no longer be empty string and array respectively

- **Prior:** `field` and `list.id` could be empty string in `EntryList`
  - **Now:** `field` and `list.id` can no longer be empty strings

- **Prior:** `field` could be empty string in `EntryExists`
  - **Now:** `field` can no longer be empty string

- **Prior:** `field` could be empty string in `EntryNested`
  - **Now:** `field` can no longer be empty string

- **Prior:** `entries` could be empty array in `EntryNested`
  - **Now:** `entries` can no longer be empty array
This commit is contained in:
Yara Tercero 2020-07-21 21:00:46 -04:00 committed by GitHub
parent 073bd66a86
commit 9c7d65cfc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1500 additions and 589 deletions

View file

@ -142,7 +142,7 @@ describe('create_endpoint_list_item_schema', () => {
expect(message.schema).toEqual({});
});
test('it should validate an undefined for "entries" but return an array', () => {
test('it should NOT validate an undefined for "entries"', () => {
const inputPayload = getCreateEndpointListItemSchemaMock();
const outputPayload = getCreateEndpointListItemSchemaMock();
delete inputPayload.entries;
@ -151,8 +151,10 @@ describe('create_endpoint_list_item_schema', () => {
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateEndpointListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {

View file

@ -20,7 +20,7 @@ import {
tags,
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types';
import { CreateCommentsArray, DefaultCreateCommentsArray, nonEmptyEntriesArray } from '../types';
import { EntriesArray } from '../types/entries';
import { DefaultUuid } from '../../siem_common_deps';
@ -28,6 +28,7 @@ export const createEndpointListItemSchema = t.intersection([
t.exact(
t.type({
description,
entries: nonEmptyEntriesArray,
name,
type: exceptionListItemType,
})
@ -36,7 +37,6 @@ export const createEndpointListItemSchema = t.intersection([
t.partial({
_tags, // defaults to empty array if not set during decode
comments: DefaultCreateCommentsArray, // 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
tags, // defaults to empty array if not set during decode

View file

@ -130,7 +130,7 @@ describe('create_exception_list_item_schema', () => {
expect(message.schema).toEqual({});
});
test('it should validate an undefined for "entries" but return an array', () => {
test('it should NOT validate an undefined for "entries"', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.entries;
@ -139,8 +139,10 @@ describe('create_exception_list_item_schema', () => {
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {

View file

@ -25,8 +25,8 @@ import { RequiredKeepUndefined } from '../../types';
import {
CreateCommentsArray,
DefaultCreateCommentsArray,
DefaultEntryArray,
NamespaceType,
nonEmptyEntriesArray,
} from '../types';
import { EntriesArray } from '../types/entries';
import { DefaultUuid } from '../../siem_common_deps';
@ -35,6 +35,7 @@ export const createExceptionListItemSchema = t.intersection([
t.exact(
t.type({
description,
entries: nonEmptyEntriesArray,
list_id,
name,
type: exceptionListItemType,
@ -44,7 +45,6 @@ export const createExceptionListItemSchema = t.intersection([
t.partial({
_tags, // defaults to empty array if not set during decode
comments: DefaultCreateCommentsArray, // 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
namespace_type, // defaults to 'single' if not set during decode

View file

@ -97,7 +97,7 @@ describe('update_endpoint_list_item_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "entries" but return an array', () => {
test('it should NOT accept an undefined for "entries"', () => {
const inputPayload = getUpdateEndpointListItemSchemaMock();
const outputPayload = getUpdateEndpointListItemSchemaMock();
delete inputPayload.entries;
@ -105,8 +105,10 @@ describe('update_endpoint_list_item_schema', () => {
const decoded = updateEndpointListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should accept an undefined for "tags" but return an array', () => {

View file

@ -22,16 +22,17 @@ import {
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
import {
DefaultEntryArray,
DefaultUpdateCommentsArray,
EntriesArray,
UpdateCommentsArray,
nonEmptyEntriesArray,
} from '../types';
export const updateEndpointListItemSchema = t.intersection([
t.exact(
t.type({
description,
entries: nonEmptyEntriesArray,
name,
type: exceptionListItemType,
})
@ -41,7 +42,6 @@ export const updateEndpointListItemSchema = t.intersection([
_tags, // defaults to empty array if not set during decode
_version, // defaults to undefined if not set during decode
comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode
entries: DefaultEntryArray, // defaults to empty array if not set during decode
id, // defaults to undefined if not set during decode
item_id: t.union([t.string, t.undefined]),
meta, // defaults to undefined if not set during decode

View file

@ -97,7 +97,7 @@ describe('update_exception_list_item_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "entries" but return an array', () => {
test('it should NOT accept an undefined for "entries"', () => {
const inputPayload = getUpdateExceptionListItemSchemaMock();
const outputPayload = getUpdateExceptionListItemSchemaMock();
delete inputPayload.entries;
@ -105,8 +105,10 @@ describe('update_exception_list_item_schema', () => {
const decoded = updateExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should accept an undefined for "namespace_type" but return enum "single"', () => {

View file

@ -23,17 +23,18 @@ import {
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
import {
DefaultEntryArray,
DefaultUpdateCommentsArray,
EntriesArray,
NamespaceType,
UpdateCommentsArray,
nonEmptyEntriesArray,
} from '../types';
export const updateExceptionListItemSchema = t.intersection([
t.exact(
t.type({
description,
entries: nonEmptyEntriesArray,
name,
type: exceptionListItemType,
})
@ -43,7 +44,6 @@ export const updateExceptionListItemSchema = t.intersection([
_tags, // defaults to empty array if not set during decode
_version, // defaults to undefined if not set during decode
comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode
entries: DefaultEntryArray, // defaults to empty array if not set during decode
id, // defaults to undefined if not set during decode
item_id: t.union([t.string, t.undefined]),
meta, // defaults to undefined if not set during decode

View file

@ -1,99 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../siem_common_deps';
import { DefaultEntryArray } from './default_entries_array';
import { EntriesArray } from './entries';
import { getEntriesArrayMock, getEntryMatchMock, getEntryNestedMock } from './entries.mock';
// NOTE: This may seem weird, but when validating schemas that use a union
// it checks against every item in that union. Since entries consist of 5
// different entry types, it returns 5 of these. To make more readable,
// extracted here.
const returnedSchemaError =
'"Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, list: {| id: string, type: "binary" | "boolean" | "byte" | "date" | "date_nanos" | "date_range" | "double" | "double_range" | "float" | "float_range" | "geo_point" | "geo_shape" | "half_float" | "integer" | "integer_range" | "ip" | "ip_range" | "keyword" | "long" | "long_range" | "shape" | "short" | "text" |}, operator: "excluded" | "included", type: "list" |} | {| field: string, operator: "excluded" | "included", type: "exists" |} | {| entries: Array<{| field: string, operator: "excluded" | "included", type: "match", value: string |}>, field: string, type: "nested" |})>"';
describe('default_entries_array', () => {
test('it should validate an empty array', () => {
const payload: EntriesArray = [];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of regular and nested entries', () => {
const payload: EntriesArray = getEntriesArrayMock();
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of nested entries', () => {
const payload: EntriesArray = [{ ...getEntryNestedMock() }];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of non nested entries', () => {
const payload: EntriesArray = [{ ...getEntryMatchMock() }];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate an array of numbers', () => {
const payload = [1];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
// TODO: Known weird error formatting that is on our list to address
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
`Invalid value "1" supplied to ${returnedSchemaError}`,
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an array of strings', () => {
const payload = ['some string'];
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
`Invalid value "some string" supplied to ${returnedSchemaError}`,
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultEntryArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { EntriesArray, entriesArray } from './entries';
/**
* Types the DefaultEntriesArray as:
* - If null or undefined, then a default array of type entry will be set
*/
export const DefaultEntryArray = new t.Type<EntriesArray, EntriesArray, unknown>(
'DefaultEntryArray',
entriesArray.is,
(input): Either<t.Errors, EntriesArray> =>
input == null ? t.success([]) : entriesArray.decode(input),
t.identity
);

View file

@ -4,65 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
ENTRY_VALUE,
EXISTS,
FIELD,
LIST,
LIST_ID,
MATCH,
MATCH_ANY,
NESTED,
OPERATOR,
TYPE,
} from '../../constants.mock';
import {
EntriesArray,
EntryExists,
EntryList,
EntryMatch,
EntryMatchAny,
EntryNested,
} from './entries';
export const getEntryMatchMock = (): EntryMatch => ({
field: FIELD,
operator: OPERATOR,
type: MATCH,
value: ENTRY_VALUE,
});
export const getEntryMatchAnyMock = (): EntryMatchAny => ({
field: FIELD,
operator: OPERATOR,
type: MATCH_ANY,
value: [ENTRY_VALUE],
});
export const getEntryListMock = (): EntryList => ({
field: FIELD,
list: { id: LIST_ID, type: TYPE },
operator: OPERATOR,
type: LIST,
});
export const getEntryExistsMock = (): EntryExists => ({
field: FIELD,
operator: OPERATOR,
type: EXISTS,
});
export const getEntryNestedMock = (): EntryNested => ({
entries: [getEntryMatchMock(), getEntryMatchMock()],
field: FIELD,
type: NESTED,
});
import { EntriesArray } from './entries';
import { getEntryMatchMock } from './entry_match.mock';
import { getEntryMatchAnyMock } from './entry_match_any.mock';
import { getEntryListMock } from './entry_list.mock';
import { getEntryExistsMock } from './entry_exists.mock';
import { getEntryNestedMock } from './entry_nested.mock';
export const getEntriesArrayMock = (): EntriesArray => [
getEntryMatchMock(),
getEntryMatchAnyMock(),
getEntryListMock(),
getEntryExistsMock(),
getEntryNestedMock(),
{ ...getEntryMatchMock() },
{ ...getEntryMatchAnyMock() },
{ ...getEntryListMock() },
{ ...getEntryExistsMock() },
{ ...getEntryNestedMock() },
];

View file

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

View file

@ -8,62 +8,19 @@
import * as t from 'io-ts';
import { operator, type } from '../common/schemas';
import { DefaultStringArray } from '../../siem_common_deps';
export const entriesMatch = t.exact(
t.type({
field: t.string,
operator,
type: t.keyof({ match: null }),
value: t.string,
})
);
export type EntryMatch = t.TypeOf<typeof entriesMatch>;
export const entriesMatchAny = t.exact(
t.type({
field: t.string,
operator,
type: t.keyof({ match_any: null }),
value: DefaultStringArray,
})
);
export type EntryMatchAny = t.TypeOf<typeof entriesMatchAny>;
export const entriesList = t.exact(
t.type({
field: t.string,
list: t.exact(t.type({ id: t.string, type })),
operator,
type: t.keyof({ list: null }),
})
);
export type EntryList = t.TypeOf<typeof entriesList>;
export const entriesExists = t.exact(
t.type({
field: t.string,
operator,
type: t.keyof({ exists: null }),
})
);
export type EntryExists = t.TypeOf<typeof entriesExists>;
export const entriesNested = t.exact(
t.type({
entries: t.array(entriesMatch),
field: t.string,
type: t.keyof({ nested: null }),
})
);
export type EntryNested = t.TypeOf<typeof entriesNested>;
import { entriesMatchAny } from './entry_match_any';
import { entriesMatch } from './entry_match';
import { entriesExists } from './entry_exists';
import { entriesList } from './entry_list';
import { entriesNested } from './entry_nested';
export const entry = t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists]);
export type Entry = t.TypeOf<typeof entry>;
export const entriesArray = t.array(
t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists, entriesNested])
);
export type EntriesArray = t.TypeOf<typeof entriesArray>;
export const entriesArrayOrUndefined = t.union([entriesArray, t.undefined]);
export type EntriesArrayOrUndefined = t.TypeOf<typeof entriesArrayOrUndefined>;

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EXISTS, FIELD, OPERATOR } from '../../constants.mock';
import { EntryExists } from './entry_exists';
export const getEntryExistsMock = (): EntryExists => ({
field: FIELD,
operator: OPERATOR,
type: EXISTS,
});

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../siem_common_deps';
import { getEntryExistsMock } from './entry_exists.mock';
import { EntryExists, entriesExists } from './entry_exists';
describe('entriesExists', () => {
test('it should validate an entry', () => {
const payload = { ...getEntryExistsMock() };
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "included"', () => {
const payload = { ...getEntryExistsMock() };
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "excluded"', () => {
const payload = { ...getEntryExistsMock() };
payload.operator = 'excluded';
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when "field" is empty string', () => {
const payload: Omit<EntryExists, 'field'> & { field: string } = {
...getEntryExistsMock(),
field: '',
};
const decoded = entriesExists.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 strip out extra keys', () => {
const payload: EntryExists & {
extraKey?: string;
} = { ...getEntryExistsMock() };
payload.extraKey = 'some extra key';
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({ ...getEntryExistsMock() });
});
test('it should not validate when "type" is not "exists"', () => {
const payload: Omit<EntryExists, 'type'> & { type: string } = {
...getEntryExistsMock(),
type: 'match',
};
const decoded = entriesExists.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import * as t from 'io-ts';
import { NonEmptyString } from '../../siem_common_deps';
import { operator } from '../common/schemas';
export const entriesExists = t.exact(
t.type({
field: NonEmptyString,
operator,
type: t.keyof({ exists: null }),
})
);
export type EntryExists = t.TypeOf<typeof entriesExists>;

View file

@ -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 { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants.mock';
import { EntryList } from './entry_list';
export const getEntryListMock = (): EntryList => ({
field: FIELD,
list: { id: LIST_ID, type: TYPE },
operator: OPERATOR,
type: LIST,
});

View file

@ -0,0 +1,95 @@
/*
* 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 { getEntryListMock } from './entry_list.mock';
import { EntryList, entriesList } from './entry_list';
describe('entriesList', () => {
test('it should validate an entry', () => {
const payload = { ...getEntryListMock() };
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "included"', () => {
const payload = { ...getEntryListMock() };
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "excluded"', () => {
const payload = { ...getEntryListMock() };
payload.operator = 'excluded';
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when "list" is not expected value', () => {
const payload: Omit<EntryList, 'list'> & { list: string } = {
...getEntryListMock(),
list: 'someListId',
};
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "someListId" supplied to "list"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate when "list.id" is empty string', () => {
const payload: Omit<EntryList, 'list'> & { list: { id: string; type: 'ip' } } = {
...getEntryListMock(),
list: { id: '', type: 'ip' },
};
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "list,id"']);
expect(message.schema).toEqual({});
});
test('it should not validate when "type" is not "lists"', () => {
const payload: Omit<EntryList, 'type'> & { type: 'match_any' } = {
...getEntryListMock(),
type: 'match_any',
};
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "match_any" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryList & {
extraKey?: string;
} = { ...getEntryListMock() };
payload.extraKey = 'some extra key';
const decoded = entriesList.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({ ...getEntryListMock() });
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import * as t from 'io-ts';
import { NonEmptyString } from '../../siem_common_deps';
import { operator, type } from '../common/schemas';
export const entriesList = t.exact(
t.type({
field: NonEmptyString,
list: t.exact(t.type({ id: NonEmptyString, type })),
operator,
type: t.keyof({ list: null }),
})
);
export type EntryList = t.TypeOf<typeof entriesList>;

View file

@ -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 { EntryMatch } from './entry_match';
export const getEntryMatchMock = (): EntryMatch => ({
field: FIELD,
operator: OPERATOR,
type: MATCH,
value: ENTRY_VALUE,
});

View file

@ -0,0 +1,107 @@
/*
* 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 { getEntryMatchMock } from './entry_match.mock';
import { EntryMatch, entriesMatch } from './entry_match';
describe('entriesMatch', () => {
test('it should validate an entry', () => {
const payload = { ...getEntryMatchMock() };
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "included"', () => {
const payload = { ...getEntryMatchMock() };
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when "operator" is "excluded"', () => {
const payload = { ...getEntryMatchMock() };
payload.operator = 'excluded';
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when "field" is empty string', () => {
const payload: Omit<EntryMatch, 'field'> & { field: string } = {
...getEntryMatchMock(),
field: '',
};
const decoded = entriesMatch.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 not validate when "value" is not string', () => {
const payload: Omit<EntryMatch, 'value'> & { value: string[] } = {
...getEntryMatchMock(),
value: ['some value'],
};
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "["some value"]" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate when "value" is empty string', () => {
const payload: Omit<EntryMatch, 'value'> & { value: string } = {
...getEntryMatchMock(),
value: '',
};
const decoded = entriesMatch.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 not validate when "type" is not "match"', () => {
const payload: Omit<EntryMatch, 'type'> & { type: string } = {
...getEntryMatchMock(),
type: 'match_any',
};
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "match_any" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryMatch & {
extraKey?: string;
} = { ...getEntryMatchMock() };
payload.extraKey = 'some value';
const decoded = entriesMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({ ...getEntryMatchMock() });
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import * as t from 'io-ts';
import { NonEmptyString } from '../../siem_common_deps';
import { operator } from '../common/schemas';
export const entriesMatch = t.exact(
t.type({
field: NonEmptyString,
operator,
type: t.keyof({ match: null }),
value: NonEmptyString,
})
);
export type EntryMatch = t.TypeOf<typeof entriesMatch>;

View file

@ -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 { EntryMatchAny } from './entry_match_any';
export const getEntryMatchAnyMock = (): EntryMatchAny => ({
field: FIELD,
operator: OPERATOR,
type: MATCH_ANY,
value: [ENTRY_VALUE],
});

View file

@ -0,0 +1,105 @@
/*
* 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 { getEntryMatchAnyMock } from './entry_match_any.mock';
import { EntryMatchAny, entriesMatchAny } from './entry_match_any';
describe('entriesMatchAny', () => {
test('it should validate an entry', () => {
const payload = { ...getEntryMatchAnyMock() };
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "included"', () => {
const payload = { ...getEntryMatchAnyMock() };
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate when operator is "excluded"', () => {
const payload = { ...getEntryMatchAnyMock() };
payload.operator = 'excluded';
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate when field is empty string', () => {
const payload: Omit<EntryMatchAny, 'field'> & { field: string } = {
...getEntryMatchAnyMock(),
field: '',
};
const decoded = entriesMatchAny.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 not validate when value is empty array', () => {
const payload: Omit<EntryMatchAny, 'value'> & { value: string[] } = {
...getEntryMatchAnyMock(),
value: [],
};
const decoded = entriesMatchAny.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 not validate when value is not string array', () => {
const payload: Omit<EntryMatchAny, 'value'> & { value: string } = {
...getEntryMatchAnyMock(),
value: 'some string',
};
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate when "type" is not "match_any"', () => {
const payload: Omit<EntryMatchAny, 'type'> & { type: string } = {
...getEntryMatchAnyMock(),
type: 'match',
};
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EntryMatchAny & {
extraKey?: string;
} = { ...getEntryMatchAnyMock() };
payload.extraKey = 'some extra key';
const decoded = entriesMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({ ...getEntryMatchAnyMock() });
});
});

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/camelcase */
import * as t from 'io-ts';
import { NonEmptyString } from '../../siem_common_deps';
import { operator } from '../common/schemas';
import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array';
export const entriesMatchAny = t.exact(
t.type({
field: NonEmptyString,
operator,
type: t.keyof({ match_any: null }),
value: nonEmptyOrNullableStringArray,
})
);
export type EntryMatchAny = t.TypeOf<typeof entriesMatchAny>;

View file

@ -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 { EntryNested } from './entry_nested';
import { getEntryMatchMock } from './entry_match.mock';
import { getEntryMatchAnyMock } from './entry_match_any.mock';
export const getEntryNestedMock = (): EntryNested => ({
entries: [{ ...getEntryMatchMock() }, { ...getEntryMatchAnyMock() }],
field: FIELD,
type: NESTED,
});

View file

@ -0,0 +1,124 @@
/*
* 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 { getEntryNestedMock } from './entry_nested.mock';
import { EntryNested, entriesNested } from './entry_nested';
import { getEntryMatchAnyMock } from './entry_match_any.mock';
import { getEntryExistsMock } from './entry_exists.mock';
describe('entriesNested', () => {
test('it should validate a nested entry', () => {
const payload = { ...getEntryNestedMock() };
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate when "type" is not "nested"', () => {
const payload: Omit<EntryNested, 'type'> & { type: 'match' } = {
...getEntryNestedMock(),
type: 'match',
};
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
test('it should NOT validate when "field" is empty string', () => {
const payload: Omit<EntryNested, 'field'> & {
field: string;
} = { ...getEntryNestedMock(), field: '' };
const decoded = entriesNested.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 NOT validate when "field" is not a string', () => {
const payload: Omit<EntryNested, 'field'> & {
field: number;
} = { ...getEntryNestedMock(), field: 1 };
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']);
expect(message.schema).toEqual({});
});
test('it should NOT validate when "entries" is not a an array', () => {
const payload: Omit<EntryNested, 'entries'> & {
entries: string;
} = { ...getEntryNestedMock(), entries: 'im a string' };
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "im a string" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should validate when "entries" contains an entry item that is type "match"', () => {
const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] };
const decoded = entriesNested.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 validate when "entries" contains an entry item that is type "exists"', () => {
const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] };
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
entries: [
{
field: 'host.name',
operator: 'included',
type: 'exists',
},
],
field: 'host.name',
type: 'nested',
});
});
test('it should strip out extra keys', () => {
const payload: EntryNested & {
extraKey?: string;
} = { ...getEntryNestedMock() };
payload.extraKey = 'some extra key';
const decoded = entriesNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({ ...getEntryNestedMock() });
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import * as t from 'io-ts';
import { NonEmptyString } from '../../siem_common_deps';
import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array';
export const entriesNested = t.exact(
t.type({
entries: nonEmptyNestedEntriesArray,
field: NonEmptyString,
type: t.keyof({ nested: null }),
})
);
export type EntryNested = t.TypeOf<typeof entriesNested>;

View file

@ -10,5 +10,12 @@ export * from './default_comments_array';
export * from './default_create_comments_array';
export * from './default_update_comments_array';
export * from './default_namespace';
export * from './default_entries_array';
export * from './entries';
export * from './entry_match';
export * from './entry_match_any';
export * from './entry_list';
export * from './entry_exists';
export * from './entry_nested';
export * from './non_empty_entries_array';
export * from './non_empty_or_nullable_string_array';
export * from './non_empty_nested_entries_array';

View file

@ -0,0 +1,123 @@
/*
* 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 { getEntryMatchMock } from './entry_match.mock';
import { getEntryMatchAnyMock } from './entry_match_any.mock';
import { getEntryListMock } from './entry_list.mock';
import { getEntryExistsMock } from './entry_exists.mock';
import { getEntryNestedMock } from './entry_nested.mock';
import { getEntriesArrayMock } from './entries.mock';
import { nonEmptyEntriesArray } from './non_empty_entries_array';
import { EntriesArray } from './entries';
describe('non_empty_entries_array', () => {
test('it should NOT validate an empty array', () => {
const payload: EntriesArray = [];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "NonEmptyEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate "undefined"', () => {
const payload = undefined;
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "NonEmptyEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate "null"', () => {
const payload = null;
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "null" supplied to "NonEmptyEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an array of "match" entries', () => {
const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of "match_any" entries', () => {
const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of "exists" entries', () => {
const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of "list" entries', () => {
const payload: EntriesArray = [{ ...getEntryListMock() }, { ...getEntryListMock() }];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of "nested" entries', () => {
const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of entries', () => {
const payload: EntriesArray = [...getEntriesArrayMock()];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate an array of non entries', () => {
const payload = [1];
const decoded = nonEmptyEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "NonEmptyEntriesArray"',
'Invalid value "1" supplied to "NonEmptyEntriesArray"',
'Invalid value "1" supplied to "NonEmptyEntriesArray"',
'Invalid value "1" supplied to "NonEmptyEntriesArray"',
'Invalid value "1" supplied to "NonEmptyEntriesArray"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,31 @@
/*
* 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 { EntriesArray, entriesArray } from './entries';
/**
* Types the nonEmptyEntriesArray as:
* - An array of entries of length 1 or greater
*
*/
export const nonEmptyEntriesArray = new t.Type<EntriesArray, EntriesArray, unknown>(
'NonEmptyEntriesArray',
entriesArray.is,
(input, context): Either<t.Errors, EntriesArray> => {
if (Array.isArray(input) && input.length === 0) {
return t.failure(input, context);
} else {
return entriesArray.validate(input, context);
}
},
t.identity
);
export type NonEmptyEntriesArray = t.OutputOf<typeof nonEmptyEntriesArray>;
export type NonEmptyEntriesArrayDecoded = t.TypeOf<typeof nonEmptyEntriesArray>;

View file

@ -0,0 +1,131 @@
/*
* 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 { getEntryMatchMock } from './entry_match.mock';
import { getEntryMatchAnyMock } from './entry_match_any.mock';
import { getEntryExistsMock } from './entry_exists.mock';
import { getEntryNestedMock } from './entry_nested.mock';
import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array';
import { EntriesArray } from './entries';
describe('non_empty_nested_entries_array', () => {
test('it should NOT validate an empty array', () => {
const payload: EntriesArray = [];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "NonEmptyNestedEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate "undefined"', () => {
const payload = undefined;
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "NonEmptyNestedEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate "null"', () => {
const payload = null;
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "null" supplied to "NonEmptyNestedEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an array of "match" entries', () => {
const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of "match_any" entries', () => {
const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of "exists" entries', () => {
const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate an array of "nested" entries', () => {
const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an array of entries', () => {
const payload: EntriesArray = [
{ ...getEntryExistsMock() },
{ ...getEntryMatchAnyMock() },
{ ...getEntryMatchMock() },
];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate an array of non entries', () => {
const payload = [1];
const decoded = nonEmptyNestedEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"',
'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"',
'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,40 @@
/*
* 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 { entriesMatchAny } from './entry_match_any';
import { entriesMatch } from './entry_match';
import { entriesExists } from './entry_exists';
export const nestedEntriesArray = t.array(t.union([entriesMatch, entriesMatchAny, entriesExists]));
export type NestedEntriesArray = t.TypeOf<typeof nestedEntriesArray>;
/**
* Types the nonEmptyNestedEntriesArray as:
* - An array of entries of length 1 or greater
*
*/
export const nonEmptyNestedEntriesArray = new t.Type<
NestedEntriesArray,
NestedEntriesArray,
unknown
>(
'NonEmptyNestedEntriesArray',
nestedEntriesArray.is,
(input, context): Either<t.Errors, NestedEntriesArray> => {
if (Array.isArray(input) && input.length === 0) {
return t.failure(input, context);
} else {
return nestedEntriesArray.validate(input, context);
}
},
t.identity
);
export type NonEmptyNestedEntriesArray = t.OutputOf<typeof nonEmptyNestedEntriesArray>;
export type NonEmptyNestedEntriesArrayDecoded = t.TypeOf<typeof nonEmptyNestedEntriesArray>;

View file

@ -0,0 +1,69 @@
/*
* 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 { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array';
describe('nonEmptyOrNullableStringArray', () => {
test('it should NOT validate an empty array', () => {
const payload: string[] = [];
const decoded = nonEmptyOrNullableStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "NonEmptyOrNullableStringArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate "undefined"', () => {
const payload = undefined;
const decoded = nonEmptyOrNullableStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "NonEmptyOrNullableStringArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate "null"', () => {
const payload = null;
const decoded = nonEmptyOrNullableStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "null" supplied to "NonEmptyOrNullableStringArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an array of with an empty string', () => {
const payload: string[] = ['im good', ''];
const decoded = nonEmptyOrNullableStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "["im good",""]" supplied to "NonEmptyOrNullableStringArray"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an array of non strings', () => {
const payload = [1];
const decoded = nonEmptyOrNullableStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[1]" supplied to "NonEmptyOrNullableStringArray"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,34 @@
/*
* 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';
/**
* Types the nonEmptyOrNullableStringArray as:
* - An array of non empty strings of length 1 or greater
* - This differs from NonEmptyStringArray in that both input and output are type array
*
*/
export const nonEmptyOrNullableStringArray = new t.Type<string[], string[], unknown>(
'NonEmptyOrNullableStringArray',
t.array(t.string).is,
(input, context): Either<t.Errors, string[]> => {
const emptyValueFound = Array.isArray(input) && input.some((value) => value === '');
const nonStringValueFound =
Array.isArray(input) && input.some((value) => typeof value !== 'string');
if (Array.isArray(input) && (emptyValueFound || nonStringValueFound || input.length === 0)) {
return t.failure(input, context);
} else {
return t.array(t.string).validate(input, context);
}
},
t.identity
);
export type NonEmptyOrNullableStringArray = t.OutputOf<typeof nonEmptyOrNullableStringArray>;
export type NonEmptyOrNullableStringArrayDecoded = t.TypeOf<typeof nonEmptyOrNullableStringArray>;

View file

@ -8,7 +8,7 @@
"name": "Sample Endpoint Exception List",
"entries": [
{
"field": "actingProcess.file.signer",
"field": "host.ip",
"operator": "excluded",
"type": "exists"
},

View file

@ -14,7 +14,6 @@ import {
Description,
DescriptionOrUndefined,
EntriesArray,
EntriesArrayOrUndefined,
ExceptionListItemType,
ExceptionListItemTypeOrUndefined,
ExceptionListType,
@ -140,7 +139,7 @@ export interface UpdateExceptionListItemOptions {
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
comments: UpdateCommentsArray;
entries: EntriesArrayOrUndefined;
entries: EntriesArray;
id: IdOrUndefined;
itemId: ItemIdOrUndefined;
namespaceType: NamespaceType;
@ -155,7 +154,7 @@ export interface UpdateEndpointListItemOptions {
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
comments: UpdateCommentsArray;
entries: EntriesArrayOrUndefined;
entries: EntriesArray;
id: IdOrUndefined;
itemId: ItemIdOrUndefined;
name: NameOrUndefined;

View file

@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
import {
DescriptionOrUndefined,
EntriesArrayOrUndefined,
EntriesArray,
ExceptionListItemSchema,
ExceptionListItemTypeOrUndefined,
ExceptionListSoSchema,
@ -37,7 +37,7 @@ interface UpdateExceptionListItemOptions {
_version: _VersionOrUndefined;
name: NameOrUndefined;
description: DescriptionOrUndefined;
entries: EntriesArrayOrUndefined;
entries: EntriesArray;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
itemId: ItemIdOrUndefined;

View file

@ -25,6 +25,9 @@ import {
Operator,
} from '../../../lists/common/schemas';
import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getEntryMatchMock } from '../../../lists/common/schemas/types/entry_match.mock';
import { getEntryMatchAnyMock } from '../../../lists/common/schemas/types/entry_match_any.mock';
import { getEntryExistsMock } from '../../../lists/common/schemas/types/entry_exists.mock';
describe('build_exceptions_query', () => {
let exclude: boolean;
@ -295,20 +298,95 @@ describe('build_exceptions_query', () => {
const item: EntryNested = {
field: 'parent',
type: 'nested',
entries: [makeMatchEntry({ field: 'nestedField', operator: 'included' })],
entries: [
{
...getEntryMatchMock(),
field: 'nestedField',
operator: 'included',
value: 'value-1',
},
],
};
const result = buildNested({ item, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:"value-1" }');
});
test('it returns formatted query when entry item is "exists"', () => {
const item: EntryNested = {
field: 'parent',
type: 'nested',
entries: [{ ...getEntryExistsMock(), field: 'nestedField', operator: 'included' }],
};
const result = buildNested({ item, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:* }');
});
test('it returns formatted query when entry item is "exists" and operator is "excluded"', () => {
const item: EntryNested = {
field: 'parent',
type: 'nested',
entries: [{ ...getEntryExistsMock(), field: 'nestedField', operator: 'excluded' }],
};
const result = buildNested({ item, language: 'kuery' });
expect(result).toEqual('parent:{ not nestedField:* }');
});
test('it returns formatted query when entry item is "match_any"', () => {
const item: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
{
...getEntryMatchAnyMock(),
field: 'nestedField',
operator: 'included',
value: ['value1', 'value2'],
},
],
};
const result = buildNested({ item, language: 'kuery' });
expect(result).toEqual('parent:{ nestedField:("value1" or "value2") }');
});
test('it returns formatted query when entry item is "match_any" and operator is "excluded"', () => {
const item: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
{
...getEntryMatchAnyMock(),
field: 'nestedField',
operator: 'excluded',
value: ['value1', 'value2'],
},
],
};
const result = buildNested({ item, language: 'kuery' });
expect(result).toEqual('parent:{ not nestedField:("value1" or "value2") }');
});
test('it returns formatted query when multiple items in nested entry', () => {
const item: EntryNested = {
field: 'parent',
type: 'nested',
entries: [
makeMatchEntry({ field: 'nestedField', operator: 'included' }),
makeMatchEntry({ field: 'nestedFieldB', operator: 'included', value: 'value-2' }),
{
...getEntryMatchMock(),
field: 'nestedField',
operator: 'included',
value: 'value-1',
},
{
...getEntryMatchMock(),
field: 'nestedFieldB',
operator: 'included',
value: 'value-2',
},
],
};
const result = buildNested({ item, language: 'kuery' });
@ -514,7 +592,7 @@ describe('build_exceptions_query', () => {
entries,
});
const expectedQuery =
'b:("value-1" OR "value-2") AND parent:{ nestedField:"value-3" } AND NOT _exists_e';
'b:("value-1" OR "value-2") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e';
expect(query).toEqual(expectedQuery);
});
@ -576,7 +654,7 @@ describe('build_exceptions_query', () => {
language: 'kuery',
entries,
});
const expectedQuery = 'b:* and parent:{ c:"value-1" and d:"value-2" } and e:*';
const expectedQuery = 'b:* and parent:{ not c:"value-1" and d:"value-2" } and e:*';
expect(query).toEqual(expectedQuery);
});
@ -642,7 +720,8 @@ describe('build_exceptions_query', () => {
language: 'kuery',
entries,
});
const expectedQuery = 'b:"value" and parent:{ c:"valueC" and d:"valueD" } and e:"valueE"';
const expectedQuery =
'b:"value" and parent:{ not c:"valueC" and not d:"valueD" } and e:"valueE"';
expect(query).toEqual(expectedQuery);
});
@ -684,7 +763,7 @@ describe('build_exceptions_query', () => {
language: 'kuery',
entries,
});
const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ c:"valueC" }';
const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ not c:"valueC" }';
expect(query).toEqual(expectedQuery);
});
@ -800,7 +879,7 @@ describe('build_exceptions_query', () => {
exclude,
});
const expectedQuery =
'(some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and e:("value-1" or "value-2"))';
'(some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2"))';
expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]);
});

View file

@ -126,7 +126,7 @@ export const buildNested = ({
}): string => {
const { field, entries } = item;
const and = getLanguageBooleanOperator({ language, value: 'and' });
const values = entries.map((entry) => `${entry.field}:"${entry.value}"`);
const values = entries.map((entry) => evaluateValues({ item: entry, language }));
return `${field}:{ ${values.join(` ${and} `)} }`;
};

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useMemo, useCallback } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
@ -19,6 +19,7 @@ interface OperatorProps {
isClearable: boolean;
fieldTypeFilter?: string[];
fieldInputWidth?: number;
isRequired?: boolean;
onChange: (a: IFieldType[]) => void;
}
@ -29,10 +30,12 @@ export const FieldComponent: React.FC<OperatorProps> = ({
isLoading = false,
isDisabled = false,
isClearable = false,
isRequired = false,
fieldTypeFilter = [],
fieldInputWidth = 190,
onChange,
}): JSX.Element => {
const [touched, setIsTouched] = useState(false);
const getLabel = useCallback((field): string => field.name, []);
const optionsMemo = useMemo((): IFieldType[] => {
if (indexPattern != null) {
@ -74,6 +77,8 @@ export const FieldComponent: React.FC<OperatorProps> = ({
isLoading={isLoading}
isDisabled={isDisabled}
isClearable={isClearable}
isInvalid={isRequired ? touched && selectedField == null : false}
onFocus={() => setIsTouched(true)}
singleSelection={{ asPlainText: true }}
data-test-subj="fieldAutocompleteComboBox"
style={{ width: `${fieldInputWidth}px` }}

View file

@ -18,6 +18,7 @@ interface AutocompleteFieldListsProps {
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
isRequired?: boolean;
onChange: (arg: ListSchema) => void;
}
@ -28,8 +29,10 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
isLoading = false,
isDisabled = false,
isClearable = false,
isRequired = false,
onChange,
}): JSX.Element => {
const [touched, setIsTouched] = useState(false);
const { http } = useKibana().services;
const [lists, setLists] = useState<ListSchema[]>([]);
const { loading, result, start } = useFindLists();
@ -97,6 +100,8 @@ export const AutocompleteFieldListsComponent: React.FC<AutocompleteFieldListsPro
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
isInvalid={isRequired ? touched && (selectedValue == null || selectedValue === '') : false}
onFocus={() => setIsTouched(true)}
singleSelection={{ asPlainText: true }}
sortMatchesBy="startsWith"
data-test-subj="valuesAutocompleteComboBox listsComboxBox"

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { uniq } from 'lodash';
@ -22,6 +22,7 @@ interface AutocompleteFieldMatchProps {
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
isRequired?: boolean;
fieldInputWidth?: number;
onChange: (arg: string) => void;
}
@ -34,9 +35,11 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
isLoading,
isDisabled = false,
isClearable = false,
isRequired = false,
fieldInputWidth,
onChange,
}): JSX.Element => {
const [touched, setIsTouched] = useState(false);
const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({
selectedField,
operatorType: OperatorTypeEnum.MATCH,
@ -96,7 +99,8 @@ export const AutocompleteFieldMatchComponent: React.FC<AutocompleteFieldMatchPro
singleSelection={{ asPlainText: true }}
onSearchChange={onSearchChange}
onCreateOption={onChange}
isInvalid={!isValid}
isInvalid={isRequired ? touched && !isValid : false}
onFocus={() => setIsTouched(true)}
sortMatchesBy="startsWith"
data-test-subj="valuesAutocompleteComboBox matchComboxBox"
style={fieldInputWidth ? { width: `${fieldInputWidth}px` } : {}}

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui';
import { uniq } from 'lodash';
@ -22,6 +22,7 @@ interface AutocompleteFieldMatchAnyProps {
isLoading: boolean;
isDisabled: boolean;
isClearable: boolean;
isRequired?: boolean;
onChange: (arg: string[]) => void;
}
@ -33,8 +34,10 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
isLoading,
isDisabled = false,
isClearable = false,
isRequired = false,
onChange,
}): JSX.Element => {
const [touched, setIsTouched] = useState(false);
const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({
selectedField,
operatorType: OperatorTypeEnum.MATCH_ANY,
@ -92,7 +95,8 @@ export const AutocompleteFieldMatchAnyComponent: React.FC<AutocompleteFieldMatch
onChange={handleValuesChange}
onSearchChange={onSearchChange}
onCreateOption={onCreateOption}
isInvalid={!isValid}
isInvalid={isRequired ? touched && (selectedValue.length === 0 || !isValid) : !isValid}
onFocus={() => setIsTouched(true)}
delimiter=", "
data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox"
fullWidth

View file

@ -54,16 +54,16 @@ describe('helpers', () => {
});
describe('#validateParams', () => {
test('returns true if value is undefined', () => {
test('returns false if value is undefined', () => {
const isValid = validateParams(undefined, getField('@timestamp'));
expect(isValid).toBeTruthy();
expect(isValid).toBeFalsy();
});
test('returns true if value is empty string', () => {
test('returns false if value is empty string', () => {
const isValid = validateParams('', getField('@timestamp'));
expect(isValid).toBeTruthy();
expect(isValid).toBeFalsy();
});
test('returns true if type is "date" and value is valid', () => {

View file

@ -36,7 +36,7 @@ export const validateParams = (
): boolean => {
// Box would show error state if empty otherwise
if (params == null || params === '') {
return true;
return false;
}
const types = field != null && field.esTypes != null ? field.esTypes : [];

View file

@ -12,10 +12,8 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { ExceptionListItemComponent } from './builder_exception_item';
import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import {
getEntryMatchMock,
getEntryMatchAnyMock,
} from '../../../../../../lists/common/schemas/types/entries.mock';
import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock';
import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/types/entry_match_any.mock';
describe('ExceptionListItemComponent', () => {
describe('and badge logic', () => {

View file

@ -117,6 +117,7 @@ export const EntryItemComponent: React.FC<EntryItemProps> = ({
isDisabled={isLoading}
onChange={handleFieldChange}
data-test-subj="exceptionBuilderEntryField"
isRequired
/>
);
@ -170,6 +171,7 @@ export const EntryItemComponent: React.FC<EntryItemProps> = ({
isClearable={false}
indexPattern={indexPattern}
onChange={handleFieldMatchValueChange}
isRequired
data-test-subj="exceptionBuilderEntryFieldMatch"
/>
);
@ -185,6 +187,7 @@ export const EntryItemComponent: React.FC<EntryItemProps> = ({
isClearable={false}
indexPattern={indexPattern}
onChange={handleFieldMatchAnyValueChange}
isRequired
data-test-subj="exceptionBuilderEntryFieldMatchAny"
/>
);
@ -199,6 +202,7 @@ export const EntryItemComponent: React.FC<EntryItemProps> = ({
isDisabled={isLoading}
isClearable={false}
onChange={handleFieldListValueChange}
isRequired
data-test-subj="exceptionBuilderEntryFieldList"
/>
);

View file

@ -38,18 +38,20 @@ import {
existsOperator,
doesNotExistOperator,
} from '../autocomplete/operators';
import { OperatorTypeEnum, OperatorEnum } from '../../../lists_plugin_deps';
import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../lists_plugin_deps';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import {
getEntryExistsMock,
getEntryListMock,
getEntryMatchMock,
getEntryMatchAnyMock,
getEntriesArrayMock,
} from '../../../../../lists/common/schemas/types/entries.mock';
import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock';
import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock';
import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock';
import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock';
import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comments.mock';
import { getEntriesArrayMock } from '../../../../../lists/common/schemas/types/entries.mock';
import { ENTRIES } from '../../../../../lists/common/constants.mock';
import { ExceptionListItemSchema, EntriesArray } from '../../../../../lists/common/schemas';
import {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
EntriesArray,
} from '../../../../../lists/common/schemas';
import { IIndexPattern } from 'src/plugins/data/common';
describe('Exception helpers', () => {
@ -251,8 +253,8 @@ describe('Exception helpers', () => {
{
fieldName: 'host.name.host.name',
isNested: true,
operator: 'is',
value: 'some host name',
operator: 'is one of',
value: ['some host name'],
},
];
expect(result).toEqual(expected);
@ -482,7 +484,7 @@ describe('Exception helpers', () => {
});
describe('#filterExceptionItems', () => {
test('it removes empty entry items', () => {
test('it removes entry items with "value" of "undefined"', () => {
const { entries, ...rest } = getExceptionListItemSchemaMock();
const mockEmptyException: EmptyEntry = {
field: 'host.name',
@ -500,6 +502,85 @@ describe('Exception helpers', () => {
expect(exceptions).toEqual([getExceptionListItemSchemaMock()]);
});
test('it removes "match" entry items with "value" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EmptyEntry = {
field: 'host.name',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
value: '',
};
const output: Array<
ExceptionListItemSchema | CreateExceptionListItemSchema
> = filterExceptionItems([
{
...rest,
entries: [...entries, mockEmptyException],
},
]);
expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
});
test('it removes "match" entry items with "field" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EmptyEntry = {
field: '',
type: OperatorTypeEnum.MATCH,
operator: OperatorEnum.INCLUDED,
value: 'some value',
};
const output: Array<
ExceptionListItemSchema | CreateExceptionListItemSchema
> = filterExceptionItems([
{
...rest,
entries: [...entries, mockEmptyException],
},
]);
expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
});
test('it removes "match_any" entry items with "field" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EmptyEntry = {
field: '',
type: OperatorTypeEnum.MATCH_ANY,
operator: OperatorEnum.INCLUDED,
value: ['some value'],
};
const output: Array<
ExceptionListItemSchema | CreateExceptionListItemSchema
> = filterExceptionItems([
{
...rest,
entries: [...entries, mockEmptyException],
},
]);
expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
});
test('it removes "nested" entry items with "field" of empty string', () => {
const { entries, ...rest } = { ...getExceptionListItemSchemaMock() };
const mockEmptyException: EntryNested = {
field: '',
type: OperatorTypeEnum.NESTED,
entries: [{ ...getEntryMatchMock() }],
};
const output: Array<
ExceptionListItemSchema | CreateExceptionListItemSchema
> = filterExceptionItems([
{
...rest,
entries: [...entries, mockEmptyException],
},
]);
expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]);
});
test('it removes `temporaryId` from items', () => {
const { meta, ...rest } = getNewExceptionItem({
listType: 'detection',
@ -509,7 +590,7 @@ describe('Exception helpers', () => {
});
const exceptions = filterExceptionItems([{ ...rest, meta }]);
expect(exceptions).toEqual([{ ...rest, meta: undefined }]);
expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]);
});
});

View file

@ -39,6 +39,7 @@ import {
EntryNested,
} from '../../../lists_plugin_deps';
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { validate } from '../../../../common/validate';
import { TimelineNonEcsData } from '../../../graphql/types';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
@ -348,11 +349,22 @@ export const filterExceptionItems = (
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
return exceptions.reduce<Array<ExceptionListItemSchema | CreateExceptionListItemSchema>>(
(acc, exception) => {
const entries = exception.entries.filter((t) => entry.is(t) || entriesNested.is(t));
const entries = exception.entries.filter((t) => {
const [validatedEntry] = validate(t, entry);
const [validatedNestedEntry] = validate(t, entriesNested);
if (validatedEntry != null || validatedNestedEntry != null) {
return true;
}
return false;
});
const item = { ...exception, entries };
if (exceptionListItemSchema.is(item)) {
return [...acc, item];
} else if (createExceptionListItemSchema.is(item) && item.meta != null) {
} else if (createExceptionListItemSchema.is(item)) {
const { meta, ...rest } = item;
const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined };
return [...acc, itemSansMetaId];

View file

@ -8,7 +8,7 @@ import { ExceptionListClient } from '../../../../../lists/server';
import { listMock } from '../../../../../lists/server/mocks';
import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types/entries';
import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types';
import { buildArtifact, getFullEndpointExceptionList } from './lists';
import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts';

View file

@ -9,7 +9,7 @@ import { deflate } from 'zlib';
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas';
import { validate } from '../../../../common/validate';
import { Entry, EntryNested } from '../../../../../lists/common/schemas/types/entries';
import { Entry, EntryNested } from '../../../../../lists/common/schemas/types';
import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema';
import { ExceptionListClient } from '../../../../../lists/server';
import { ENDPOINT_LIST_ID } from '../../../../common/shared_imports';