[Security Solution][Exceptions] Add lowercase normalizer for case-insensitivity + deprecate _tags field (new OS field) (#77379)

* Finish adding .lower to exceptionable fields

* Add back migrations

* .lower -> .caseless

* Add separate field for os type

* updates

* Type updates

* Switch over to osTypes

* get rid of _tags

* Add tests for schema validation

* Remove remaining references to _tags

* Another round of test fixes

* DefaultArray tests

* More test fixes

* Fix remaining test failures

* types / tests

* more test updates

* lowercase os values

* Address feedback + fix test failure

* tests

* Fix integration test

* process.executable.path -> process.executable.caseless

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Madison Caldwell 2020-10-02 15:54:43 -04:00 committed by GitHub
parent b66de2ce1d
commit c456f64a7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 636 additions and 479 deletions

View file

@ -212,6 +212,37 @@ test('tests processing keyword field with multi fields with analyzed text field'
expect(mappings).toEqual(keywordWithAnalyzedMultiFieldsMapping);
});
test('tests processing keyword field with multi fields with normalized keyword field', () => {
const keywordWithNormalizedMultiFieldsLiteralYml = `
- name: keywordWithNormalizedMultiField
type: keyword
multi_fields:
- name: normalized
type: keyword
normalizer: lowercase
`;
const keywordWithNormalizedMultiFieldsMapping = {
properties: {
keywordWithNormalizedMultiField: {
ignore_above: 1024,
type: 'keyword',
fields: {
normalized: {
type: 'keyword',
ignore_above: 1024,
normalizer: 'lowercase',
},
},
},
},
};
const fields: Field[] = safeLoad(keywordWithNormalizedMultiFieldsLiteralYml);
const processedFields = processFields(fields);
const mappings = generateMappings(processedFields);
expect(mappings).toEqual(keywordWithNormalizedMultiFieldsMapping);
});
test('tests processing object field with no other attributes', () => {
const objectFieldLiteralYml = `
- name: objectField

View file

@ -189,6 +189,9 @@ function generateKeywordMapping(field: Field): IndexTemplateMapping {
if (field.ignore_above) {
mapping.ignore_above = field.ignore_above;
}
if (field.normalizer) {
mapping.normalizer = field.normalizer;
}
return mapping;
}

View file

@ -20,6 +20,7 @@ export interface Field {
index?: boolean;
required?: boolean;
multi_fields?: Fields;
normalizer?: string;
doc_values?: boolean;
copy_to?: string;
analyzer?: string;

View file

@ -113,12 +113,6 @@ You should see the new exception list created like so:
```sh
{
"_tags": [
"endpoint",
"process",
"malware",
"os:linux"
],
"created_at": "2020-05-28T19:16:31.052Z",
"created_by": "yo",
"description": "This is a sample endpoint type exception",
@ -141,12 +135,6 @@ And you can attach exception list items like so:
```ts
{
"_tags": [
"endpoint",
"process",
"malware",
"os:linux"
],
"comments": [],
"created_at": "2020-05-28T19:17:21.099Z",
"created_by": "yo",
@ -173,6 +161,7 @@ And you can attach exception list items like so:
"list_id": "endpoint_list",
"name": "Sample Endpoint Exception List",
"namespace_type": "single",
"os_types": ["linux"],
"tags": [
"user added string for a tag",
"malware"
@ -222,12 +211,6 @@ or for finding exception lists:
{
"data": [
{
"_tags": [
"endpoint",
"process",
"malware",
"os:linux"
],
"created_at": "2020-05-28T19:16:31.052Z",
"created_by": "yo",
"description": "This is a sample endpoint type exception",
@ -235,6 +218,7 @@ or for finding exception lists:
"list_id": "endpoint_list",
"name": "Sample Endpoint Exception List",
"namespace_type": "single",
"os_types": ["linux"],
"tags": [
"user added string for a tag",
"malware"

View file

@ -5,6 +5,7 @@
*/
import moment from 'moment';
import { OsTypeArray } from './schemas/common';
import { EntriesArray } from './schemas/types';
import { EndpointEntriesArray } from './schemas/types/endpoint';
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
@ -68,7 +69,7 @@ export const ENDPOINT_ENTRIES: EndpointEntriesArray = [
{ field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' },
];
export const ITEM_TYPE = 'simple';
export const _TAGS = [];
export const OS_TYPES: OsTypeArray = ['windows'];
export const TAGS = [];
export const COMMENTS = [];
export const FILTER = 'name:Nicolas Bourbaki';

View file

@ -27,6 +27,8 @@ import {
esDataTypeUnion,
exceptionListType,
operator,
osType,
osTypeArrayOrUndefined,
type,
} from './schemas';
@ -379,4 +381,35 @@ describe('Common schemas', () => {
expect(message.schema).toEqual({});
});
});
describe('osType', () => {
test('it will validate a correct osType', () => {
const payload = 'windows';
const decoded = osType.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it will fail to validate an incorrect osType', () => {
const payload = 'foo';
const decoded = osType.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "foo" supplied to ""linux" | "macos" | "windows""',
]);
expect(message.schema).toEqual({});
});
test('it will default to an empty array when osTypeArrayOrUndefined is used', () => {
const payload = undefined;
const decoded = osTypeArrayOrUndefined.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});
});

View file

@ -9,7 +9,7 @@
import * as t from 'io-ts';
import { DefaultNamespace } from '../types/default_namespace';
import { DefaultStringArray, NonEmptyString } from '../../shared_imports';
import { DefaultArray, DefaultStringArray, NonEmptyString } from '../../shared_imports';
export const name = t.string;
export type Name = t.TypeOf<typeof name>;
@ -211,11 +211,6 @@ export type Tags = t.TypeOf<typeof tags>;
export const tagsOrUndefined = t.union([tags, t.undefined]);
export type TagsOrUndefined = t.TypeOf<typeof tagsOrUndefined>;
export const _tags = DefaultStringArray;
export type _Tags = t.TypeOf<typeof _tags>;
export const _tagsOrUndefined = t.union([_tags, t.undefined]);
export type _TagsOrUndefined = t.TypeOf<typeof _tagsOrUndefined>;
export const exceptionListType = t.keyof({ detection: null, endpoint: null });
export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]);
export type ExceptionListType = t.TypeOf<typeof exceptionListType>;
@ -317,3 +312,16 @@ export type Immutable = t.TypeOf<typeof immutable>;
export const immutableOrUndefined = t.union([immutable, t.undefined]);
export type ImmutableOrUndefined = t.TypeOf<typeof immutableOrUndefined>;
export const osType = t.keyof({
linux: null,
macos: null,
windows: null,
});
export type OsType = t.TypeOf<typeof osType>;
export const osTypeArray = DefaultArray(osType);
export type OsTypeArray = t.TypeOf<typeof osTypeArray>;
export const osTypeArrayOrUndefined = t.union([osTypeArray, t.undefined]);
export type OsTypeArrayOrUndefined = t.OutputOf<typeof osTypeArray>;

View file

@ -11,20 +11,20 @@ import {
ITEM_TYPE,
META,
NAME,
OS_TYPES,
TAGS,
_TAGS,
} from '../../constants.mock';
import { CreateEndpointListItemSchema } from './create_endpoint_list_item_schema';
export const getCreateEndpointListItemSchemaMock = (): CreateEndpointListItemSchema => ({
_tags: _TAGS,
comments: COMMENTS,
description: DESCRIPTION,
entries: ENDPOINT_ENTRIES,
item_id: undefined,
meta: META,
name: NAME,
os_types: OS_TYPES,
tags: TAGS,
type: ITEM_TYPE,
});

View file

@ -174,19 +174,6 @@ describe('create_endpoint_list_item_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should pass validation when supplied an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
const inputPayload = getCreateEndpointListItemSchemaMock();
const outputPayload = getCreateEndpointListItemSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
const decoded = createEndpointListItemSchema.decode(inputPayload);
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);
});
test('it should pass validation when supplied an undefined for "item_id" and auto generate a uuid', () => {
const inputPayload = getCreateEndpointListItemSchemaMock();
delete inputPayload.item_id;

View file

@ -8,13 +8,13 @@ import * as t from 'io-ts';
import {
ItemId,
OsTypeArray,
Tags,
_Tags,
_tags,
description,
exceptionListItemType,
meta,
name,
osTypeArrayOrUndefined,
tags,
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
@ -34,10 +34,10 @@ export const createEndpointListItemSchema = t.intersection([
),
t.exact(
t.partial({
_tags, // defaults to empty array if not set during decode
comments: DefaultCreateCommentsArray, // 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
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@ -48,11 +48,11 @@ export type CreateEndpointListItemSchema = t.OutputOf<typeof createEndpointListI
// This type is used after a decode since some things are defaults after a decode.
export type CreateEndpointListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof createEndpointListItemSchema>>,
'_tags' | 'tags' | 'item_id' | 'entries' | 'comments'
'tags' | 'item_id' | 'entries' | 'comments' | 'os_types'
> & {
_tags: _Tags;
comments: CreateCommentsArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
os_types: OsTypeArray;
};

View file

@ -14,14 +14,13 @@ import {
META,
NAME,
NAMESPACE_TYPE,
OS_TYPES,
TAGS,
_TAGS,
} from '../../constants.mock';
import { CreateExceptionListItemSchema } from './create_exception_list_item_schema';
export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({
_tags: _TAGS,
comments: COMMENTS,
description: DESCRIPTION,
entries: ENTRIES,
@ -30,6 +29,7 @@ export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemS
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
os_types: OS_TYPES,
tags: TAGS,
type: ITEM_TYPE,
});
@ -43,6 +43,7 @@ export const getCreateExceptionListItemMinimalSchemaMock = (): CreateExceptionLi
item_id: ITEM_ID,
list_id: LIST_ID,
name: NAME,
os_types: OS_TYPES,
type: ITEM_TYPE,
});
@ -54,5 +55,6 @@ export const getCreateExceptionListItemMinimalSchemaMockWithoutId = (): CreateEx
entries: ENTRIES,
list_id: LIST_ID,
name: NAME,
os_types: OS_TYPES,
type: ITEM_TYPE,
});

View file

@ -176,19 +176,6 @@ describe('create_exception_list_item_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should pass validation when supplied an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
const outputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
const decoded = createExceptionListItemSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListItemSchema).item_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should pass validation when supplied an undefined for "item_id" and auto generate a uuid', () => {
const inputPayload = getCreateExceptionListItemSchemaMock();
delete inputPayload.item_id;

View file

@ -8,15 +8,15 @@ import * as t from 'io-ts';
import {
ItemId,
OsTypeArray,
Tags,
_Tags,
_tags,
description,
exceptionListItemType,
list_id,
meta,
name,
namespace_type,
osTypeArrayOrUndefined,
tags,
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
@ -41,11 +41,11 @@ export const createExceptionListItemSchema = t.intersection([
),
t.exact(
t.partial({
_tags, // defaults to empty array if not set during decode
comments: DefaultCreateCommentsArray, // 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
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@ -56,12 +56,12 @@ export type CreateExceptionListItemSchema = t.OutputOf<typeof createExceptionLis
// This type is used after a decode since some things are defaults after a decode.
export type CreateExceptionListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof createExceptionListItemSchema>>,
'_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
> & {
_tags: _Tags;
comments: CreateCommentsArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
namespace_type: NamespaceType;
os_types: OsTypeArray;
};

View file

@ -17,12 +17,12 @@ import {
import { CreateExceptionListSchema } from './create_exception_list_schema';
export const getCreateExceptionListSchemaMock = (): CreateExceptionListSchema => ({
_tags: [],
description: DESCRIPTION,
list_id: undefined,
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
os_types: [],
tags: [],
type: ENDPOINT_TYPE,
version: VERSION,

View file

@ -50,19 +50,6 @@ describe('create_exception_list_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "_tags" but return an array and generate a correct body not counting the uuid', () => {
const inputPayload = getCreateExceptionListSchemaMock();
const outputPayload = getCreateExceptionListSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
const decoded = createExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
delete (message.schema as CreateExceptionListSchema).list_id;
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "list_id" and auto generate a uuid', () => {
const inputPayload = getCreateExceptionListSchemaMock();
delete inputPayload.list_id;

View file

@ -8,14 +8,14 @@ import * as t from 'io-ts';
import {
ListId,
OsTypeArray,
Tags,
_Tags,
_tags,
description,
exceptionListType,
meta,
name,
namespace_type,
osTypeArrayOrUndefined,
tags,
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
@ -36,10 +36,10 @@ export const createExceptionListSchema = t.intersection([
),
t.exact(
t.partial({
_tags, // defaults to empty array if not set during decode
list_id: DefaultUuid, // defaults to a GUID (UUID v4) string if not set during decode
meta, // defaults to undefined if not set during decode
namespace_type, // defaults to 'single' if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
version: DefaultVersionNumber, // defaults to numerical 1 if not set during decode
})
@ -51,11 +51,11 @@ export type CreateExceptionListSchema = t.OutputOf<typeof createExceptionListSch
// This type is used after a decode since some things are defaults after a decode.
export type CreateExceptionListSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof createExceptionListSchema>>,
'_tags' | 'tags' | 'list_id' | 'namespace_type'
'tags' | 'list_id' | 'namespace_type' | 'os_types'
> & {
_tags: _Tags;
tags: Tags;
list_id: ListId;
namespace_type: NamespaceType;
os_types: OsTypeArray;
version: DefaultVersionNumberDecoded;
};

View file

@ -13,14 +13,13 @@ import {
LIST_ITEM_ID,
META,
NAME,
OS_TYPES,
TAGS,
_TAGS,
} from '../../constants.mock';
import { UpdateEndpointListItemSchema } from './update_endpoint_list_item_schema';
export const getUpdateEndpointListItemSchemaMock = (): UpdateEndpointListItemSchema => ({
_tags: _TAGS,
_version: undefined,
comments: COMMENTS,
description: DESCRIPTION,
@ -29,6 +28,7 @@ export const getUpdateEndpointListItemSchemaMock = (): UpdateEndpointListItemSch
item_id: LIST_ITEM_ID,
meta: META,
name: NAME,
os_types: OS_TYPES,
tags: TAGS,
type: ITEM_TYPE,
});

View file

@ -127,18 +127,6 @@ describe('update_endpoint_list_item_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "_tags" but return an array', () => {
const inputPayload = getUpdateEndpointListItemSchemaMock();
const outputPayload = getUpdateEndpointListItemSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
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);
});
test('it should not allow an extra key to be sent in', () => {
const payload: UpdateEndpointListItemSchema & {
extraKey?: string;

View file

@ -7,15 +7,15 @@
import * as t from 'io-ts';
import {
OsTypeArray,
Tags,
_Tags,
_tags,
_version,
description,
exceptionListItemType,
id,
meta,
name,
osTypeArrayOrUndefined,
tags,
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
@ -37,12 +37,12 @@ export const updateEndpointListItemSchema = t.intersection([
),
t.exact(
t.partial({
_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
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
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@ -53,10 +53,10 @@ export type UpdateEndpointListItemSchema = t.OutputOf<typeof updateEndpointListI
// This type is used after a decode since some things are defaults after a decode.
export type UpdateEndpointListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof updateEndpointListItemSchema>>,
'_tags' | 'tags' | 'entries' | 'comments'
'tags' | 'entries' | 'comments'
> & {
_tags: _Tags;
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
os_types: OsTypeArray;
};

View file

@ -15,14 +15,13 @@ import {
META,
NAME,
NAMESPACE_TYPE,
OS_TYPES,
TAGS,
_TAGS,
} from '../../constants.mock';
import { UpdateExceptionListItemSchema } from './update_exception_list_item_schema';
export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({
_tags: _TAGS,
_version: undefined,
comments: COMMENTS,
description: DESCRIPTION,
@ -32,6 +31,7 @@ export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemS
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
os_types: ['linux'],
tags: TAGS,
type: ITEM_TYPE,
});
@ -45,5 +45,6 @@ export const getUpdateMinimalExceptionListItemSchemaMock = (): UpdateExceptionLi
entries: ENTRIES,
item_id: ITEM_ID,
name: NAME,
os_types: OS_TYPES,
type: ITEM_TYPE,
});

View file

@ -139,18 +139,6 @@ describe('update_exception_list_item_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "_tags" but return an array', () => {
const inputPayload = getUpdateExceptionListItemSchemaMock();
const outputPayload = getUpdateExceptionListItemSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
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);
});
test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => {
const inputPayload = getUpdateExceptionListItemSchemaMock();
delete inputPayload.item_id;

View file

@ -7,9 +7,8 @@
import * as t from 'io-ts';
import {
OsTypeArray,
Tags,
_Tags,
_tags,
_version,
description,
exceptionListItemType,
@ -17,6 +16,7 @@ import {
meta,
name,
namespace_type,
osTypeArrayOrUndefined,
tags,
} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
@ -39,13 +39,13 @@ export const updateExceptionListItemSchema = t.intersection([
),
t.exact(
t.partial({
_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
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
namespace_type, // defaults to 'single' if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@ -56,11 +56,11 @@ export type UpdateExceptionListItemSchema = t.OutputOf<typeof updateExceptionLis
// This type is used after a decode since some things are defaults after a decode.
export type UpdateExceptionListItemSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof updateExceptionListItemSchema>>,
'_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments'
'tags' | 'entries' | 'namespace_type' | 'comments' | 'os_types'
> & {
_tags: _Tags;
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
namespace_type: NamespaceType;
os_types: OsTypeArray;
};

View file

@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DESCRIPTION, ID, LIST_ID, META, NAME, NAMESPACE_TYPE, _TAGS } from '../../constants.mock';
import { DESCRIPTION, ID, LIST_ID, META, NAME, NAMESPACE_TYPE } from '../../constants.mock';
import { UpdateExceptionListSchema } from './update_exception_list_schema';
export const getUpdateExceptionListSchemaMock = (): UpdateExceptionListSchema => ({
_tags: _TAGS,
_version: undefined,
description: DESCRIPTION,
id: ID,
@ -17,6 +16,7 @@ export const getUpdateExceptionListSchemaMock = (): UpdateExceptionListSchema =>
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
os_types: [],
tags: ['malware'],
type: 'endpoint',
});

View file

@ -100,18 +100,6 @@ describe('update_exception_list_schema', () => {
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "_tags" but return an array', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
const outputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload._tags;
outputPayload._tags = [];
const decoded = updateExceptionListSchema.decode(inputPayload);
const checked = exactCheck(inputPayload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(outputPayload);
});
test('it should accept an undefined for "list_id" and generate a correct body not counting the uuid', () => {
const inputPayload = getUpdateExceptionListSchemaMock();
delete inputPayload.list_id;

View file

@ -7,9 +7,8 @@
import * as t from 'io-ts';
import {
OsTypeArray,
Tags,
_Tags,
_tags,
_version,
description,
exceptionListType,
@ -18,6 +17,7 @@ import {
meta,
name,
namespace_type,
osTypeArrayOrUndefined,
tags,
version,
} from '../common/schemas';
@ -34,12 +34,12 @@ export const updateExceptionListSchema = t.intersection([
),
t.exact(
t.partial({
_tags, // defaults to empty array if not set during decode
_version, // defaults to undefined if not set during decode
id, // defaults to undefined if not set during decode
list_id, // defaults to undefined if not set during decode
meta, // defaults to undefined if not set during decode
namespace_type, // defaults to 'single' if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
version, // defaults to undefined if not set during decode
})
@ -51,9 +51,9 @@ export type UpdateExceptionListSchema = t.OutputOf<typeof updateExceptionListSch
// This type is used after a decode since the arrays turn into defaults of empty arrays.
export type UpdateExceptionListSchemaDecoded = Omit<
RequiredKeepUndefined<t.TypeOf<typeof updateExceptionListSchema>>,
'_tags | tags | namespace_type'
'tags | namespace_type' | 'os_types'
> & {
_tags: _Tags;
tags: Tags;
namespace_type: NamespaceType;
os_types: OsTypeArray;
};

View file

@ -42,7 +42,7 @@ describe('create_endpoint_list_schema', () => {
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'invalid keys "_tags,["endpoint","process","malware","os:linux"],_version,created_at,created_by,description,id,immutable,meta,{},name,namespace_type,tags,["user added string for a tag","malware"],tie_breaker_id,type,updated_at,updated_by,version"',
'invalid keys "_version,created_at,created_by,description,id,immutable,meta,{},name,namespace_type,os_types,["linux"],tags,["user added string for a tag","malware"],tie_breaker_id,type,updated_at,updated_by,version"',
]);
expect(message.schema).toEqual({});
});

View file

@ -15,6 +15,7 @@ import {
META,
NAME,
NAMESPACE_TYPE,
OS_TYPES,
TIE_BREAKER,
USER,
} from '../../constants.mock';
@ -22,7 +23,6 @@ import {
import { ExceptionListItemSchema } from './exception_list_item_schema';
export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
_version: undefined,
comments: COMMENTS,
created_at: DATE_NOW,
@ -35,6 +35,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({
meta: META,
name: NAME,
namespace_type: NAMESPACE_TYPE,
os_types: ['linux'],
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: TIE_BREAKER,
type: ITEM_TYPE,
@ -49,7 +50,6 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({
export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = (): Partial<
ExceptionListItemSchema
> => ({
_tags: [],
comments: [],
created_by: ELASTIC_USER,
description: DESCRIPTION,
@ -58,6 +58,7 @@ export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = (): Pa
list_id: LIST_ID,
name: NAME,
namespace_type: 'single',
os_types: OS_TYPES,
tags: [],
type: ITEM_TYPE,
updated_by: ELASTIC_USER,

View file

@ -7,7 +7,6 @@
import * as t from 'io-ts';
import {
_tags,
_versionOrUndefined,
created_at,
created_by,
@ -19,6 +18,7 @@ import {
metaOrUndefined,
name,
namespace_type,
osTypeArray,
tags,
tie_breaker_id,
updated_at,
@ -28,7 +28,6 @@ import { commentsArray, entriesArray } from '../types';
export const exceptionListItemSchema = t.exact(
t.type({
_tags,
_version: _versionOrUndefined,
comments: commentsArray,
created_at,
@ -41,6 +40,7 @@ export const exceptionListItemSchema = t.exact(
meta: metaOrUndefined,
name,
namespace_type,
os_types: osTypeArray,
tags,
tie_breaker_id,
type: exceptionListItemType,

View file

@ -28,7 +28,6 @@ import {
import { ExceptionListSchema } from './exception_list_schema';
export const getExceptionListSchemaMock = (): ExceptionListSchema => ({
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
_version: _VERSION,
created_at: DATE_NOW,
created_by: USER,
@ -39,6 +38,7 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({
meta: META,
name: 'Sample Endpoint Exception List',
namespace_type: 'agnostic',
os_types: ['linux'],
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: TIE_BREAKER,
type: ENDPOINT_TYPE,
@ -63,13 +63,13 @@ export const getTrustedAppsListSchemaMock = (): ExceptionListSchema => {
export const getExceptionResponseMockWithoutAutoGeneratedValues = (): Partial<
ExceptionListSchema
> => ({
_tags: [],
created_by: ELASTIC_USER,
description: DESCRIPTION,
immutable: IMMUTABLE,
list_id: LIST_ID,
name: NAME,
namespace_type: 'single',
os_types: [],
tags: [],
type: ENDPOINT_TYPE,
updated_by: ELASTIC_USER,

View file

@ -7,7 +7,6 @@
import * as t from 'io-ts';
import {
_tags,
_versionOrUndefined,
created_at,
created_by,
@ -19,6 +18,7 @@ import {
metaOrUndefined,
name,
namespace_type,
osTypeArray,
tags,
tie_breaker_id,
updated_at,
@ -28,7 +28,6 @@ import {
export const exceptionListSchema = t.exact(
t.type({
_tags,
_version: _versionOrUndefined,
created_at,
created_by,
@ -39,6 +38,7 @@ export const exceptionListSchema = t.exact(
meta: metaOrUndefined,
name,
namespace_type,
os_types: osTypeArray,
tags,
tie_breaker_id,
type: exceptionListType,

View file

@ -8,7 +8,6 @@ import * as t from 'io-ts';
import { commentsArrayOrUndefined, entriesArrayOrUndefined } from '../types';
import {
_tags,
created_at,
created_by,
description,
@ -20,6 +19,7 @@ import {
list_type,
metaOrUndefined,
name,
osTypeArray,
tags,
tie_breaker_id,
updated_by,
@ -31,7 +31,6 @@ import {
*/
export const exceptionListSoSchema = t.exact(
t.type({
_tags,
comments: commentsArrayOrUndefined,
created_at,
created_by,
@ -43,6 +42,7 @@ export const exceptionListSoSchema = t.exact(
list_type,
meta: metaOrUndefined,
name,
os_types: osTypeArray,
tags,
tie_breaker_id,
type: t.union([exceptionListType, exceptionListItemType]),

View file

@ -41,6 +41,8 @@ export {
namespaceType,
ExceptionListType,
Type,
osTypeArray,
OsTypeArray,
} from './schemas';
export { ENDPOINT_LIST_ID } from './constants';

View file

@ -6,6 +6,7 @@
export {
NonEmptyString,
DefaultArray,
DefaultUuid,
DefaultStringArray,
DefaultVersionNumber,

View file

@ -37,13 +37,13 @@ export const createEndpointListItemRoute = (router: IRouter): void => {
try {
const {
name,
_tags,
tags,
meta,
comments,
description,
entries,
item_id: itemId,
os_types: osTypes,
type,
} = request.body;
const exceptionLists = getExceptionListClient(context);
@ -58,13 +58,13 @@ export const createEndpointListItemRoute = (router: IRouter): void => {
});
} else {
const createdList = await exceptionLists.createEndpointListItem({
_tags,
comments,
description,
entries,
itemId,
meta,
name,
osTypes,
tags,
type,
});

View file

@ -39,7 +39,6 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
const {
namespace_type: namespaceType,
name,
_tags,
tags,
meta,
comments,
@ -47,6 +46,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
entries,
item_id: itemId,
list_id: listId,
os_types: osTypes,
type,
} = request.body;
const exceptionLists = getExceptionListClient(context);
@ -87,7 +87,6 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
}
}
const createdList = await exceptionLists.createExceptionListItem({
_tags,
comments,
description,
entries,
@ -96,6 +95,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
meta,
name,
namespaceType,
osTypes,
tags,
type,
});

View file

@ -36,7 +36,6 @@ export const createExceptionListRoute = (router: IRouter): void => {
try {
const {
name,
_tags,
tags,
meta,
namespace_type: namespaceType,
@ -58,7 +57,6 @@ export const createExceptionListRoute = (router: IRouter): void => {
});
} else {
const createdList = await exceptionLists.createExceptionList({
_tags,
description,
immutable: false,
listId,

View file

@ -38,9 +38,9 @@ export const updateEndpointListItemRoute = (router: IRouter): void => {
description,
id,
name,
os_types: osTypes,
meta,
type,
_tags,
_version,
comments,
entries,
@ -49,7 +49,6 @@ export const updateEndpointListItemRoute = (router: IRouter): void => {
} = request.body;
const exceptionLists = getExceptionListClient(context);
const exceptionListItem = await exceptionLists.updateEndpointListItem({
_tags,
_version,
comments,
description,
@ -58,6 +57,7 @@ export const updateEndpointListItemRoute = (router: IRouter): void => {
itemId,
meta,
name,
osTypes,
tags,
type,
});

View file

@ -46,12 +46,12 @@ export const updateExceptionListItemRoute = (router: IRouter): void => {
name,
meta,
type,
_tags,
_version,
comments,
entries,
item_id: itemId,
namespace_type: namespaceType,
os_types: osTypes,
tags,
} = request.body;
if (id == null && itemId == null) {
@ -62,7 +62,6 @@ export const updateExceptionListItemRoute = (router: IRouter): void => {
} else {
const exceptionLists = getExceptionListClient(context);
const exceptionListItem = await exceptionLists.updateExceptionListItem({
_tags,
_version,
comments,
description,
@ -72,6 +71,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => {
meta,
name,
namespaceType,
osTypes,
tags,
type,
});

View file

@ -35,7 +35,6 @@ export const updateExceptionListRoute = (router: IRouter): void => {
const siemResponse = buildSiemResponse(response);
try {
const {
_tags,
_version,
tags,
name,
@ -44,6 +43,7 @@ export const updateExceptionListRoute = (router: IRouter): void => {
list_id: listId,
meta,
namespace_type: namespaceType,
os_types: osTypes,
type,
version,
} = request.body;
@ -55,7 +55,6 @@ export const updateExceptionListRoute = (router: IRouter): void => {
});
} else {
const list = await exceptionLists.updateExceptionList({
_tags,
_version,
description,
id,
@ -63,6 +62,7 @@ export const updateExceptionListRoute = (router: IRouter): void => {
meta,
name,
namespaceType,
osTypes,
tags,
type,
version,

View file

@ -6,6 +6,8 @@
import { SavedObjectsType } from 'kibana/server';
import { migrations } from './migrations';
export const exceptionListSavedObjectType = 'exception-list';
export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic';
export type SavedObjectType = 'exception-list' | 'exception-list-agnostic';
@ -149,6 +151,9 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
item_id: {
type: 'keyword',
},
os_types: {
type: 'keyword',
},
},
};
@ -163,6 +168,7 @@ const combinedMappings: SavedObjectsType['mappings'] = {
export const exceptionListType: SavedObjectsType = {
hidden: false,
mappings: combinedMappings,
migrations,
name: exceptionListSavedObjectType,
namespaceType: 'single',
};
@ -170,6 +176,7 @@ export const exceptionListType: SavedObjectsType = {
export const exceptionListAgnosticType: SavedObjectsType = {
hidden: false,
mappings: combinedMappings,
migrations,
name: exceptionListAgnosticSavedObjectType,
namespaceType: 'agnostic',
};

View file

@ -0,0 +1,132 @@
/*
* 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 { SavedObjectUnsanitizedDoc } from 'kibana/server';
import { ENDPOINT_LIST_ID } from '../../common/constants';
import { OldExceptionListSoSchema, migrations } from './migrations';
describe('7.10.0 lists migrations', () => {
const migration = migrations['7.10.0'];
test('properly converts .text fields to .caseless', () => {
const doc = {
attributes: {
entries: [
{
field: 'file.path.text',
operator: 'included',
type: 'match',
value: 'C:\\Windows\\explorer.exe',
},
{
field: 'host.os.name',
operator: 'included',
type: 'match',
value: 'my-host',
},
{
entries: [
{
field: 'process.command_line.text',
operator: 'included',
type: 'match',
value: '/usr/bin/bash',
},
{
field: 'process.parent.command_line.text',
operator: 'included',
type: 'match',
value: '/usr/bin/bash',
},
],
field: 'nested.field',
type: 'nested',
},
],
list_id: ENDPOINT_LIST_ID,
},
id: 'abcd',
migrationVersion: {},
references: [],
type: 'so-type',
updated_at: '2020-06-09T20:18:20.349Z',
};
expect(
migration((doc as unknown) as SavedObjectUnsanitizedDoc<OldExceptionListSoSchema>)
).toEqual({
attributes: {
entries: [
{
field: 'file.path.caseless',
operator: 'included',
type: 'match',
value: 'C:\\Windows\\explorer.exe',
},
{
field: 'host.os.name',
operator: 'included',
type: 'match',
value: 'my-host',
},
{
entries: [
{
field: 'process.command_line.caseless',
operator: 'included',
type: 'match',
value: '/usr/bin/bash',
},
{
field: 'process.parent.command_line.caseless',
operator: 'included',
type: 'match',
value: '/usr/bin/bash',
},
],
field: 'nested.field',
type: 'nested',
},
],
list_id: ENDPOINT_LIST_ID,
},
id: 'abcd',
migrationVersion: {},
references: [],
type: 'so-type',
updated_at: '2020-06-09T20:18:20.349Z',
});
});
test('properly copies os tags to os_types', () => {
const doc = {
attributes: {
_tags: ['1234', 'os:windows'],
comments: [],
},
id: 'abcd',
migrationVersion: {},
references: [],
type: 'so-type',
updated_at: '2020-06-09T20:18:20.349Z',
};
expect(
migration((doc as unknown) as SavedObjectUnsanitizedDoc<OldExceptionListSoSchema>)
).toEqual({
attributes: {
_tags: ['1234', 'os:windows'],
comments: [],
os_types: ['windows'],
},
id: 'abcd',
migrationVersion: {},
references: [],
type: 'so-type',
updated_at: '2020-06-09T20:18:20.349Z',
});
});
});

View file

@ -0,0 +1,66 @@
/*
* 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 { SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants';
import {
EntriesArray,
ExceptionListSoSchema,
NonEmptyNestedEntriesArray,
OsTypeArray,
entriesNested,
entry,
} from '../../common/schemas';
const entryType = t.union([entry, entriesNested]);
type EntryType = t.TypeOf<typeof entryType>;
const migrateEntry = (entryToMigrate: EntryType): EntryType => {
const newEntry = entryToMigrate;
if (entriesNested.is(entryToMigrate) && entriesNested.is(newEntry)) {
newEntry.entries = entryToMigrate.entries.map((nestedEntry) =>
migrateEntry(nestedEntry)
) as NonEmptyNestedEntriesArray;
}
newEntry.field = entryToMigrate.field.replace('.text', '.caseless');
return newEntry;
};
const reduceOsTypes = (acc: string[], tag: string): string[] => {
if (tag.startsWith('os:')) {
// TODO: check OS against type
return [...acc, tag.replace('os:', '')];
}
return [...acc];
};
export type OldExceptionListSoSchema = ExceptionListSoSchema & {
_tags: string[];
};
export const migrations = {
'7.10.0': (
doc: SavedObjectUnsanitizedDoc<OldExceptionListSoSchema>
): SavedObjectSanitizedDoc<ExceptionListSoSchema> => ({
...doc,
...{
attributes: {
...doc.attributes,
...(doc.attributes.entries &&
[ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID].includes(doc.attributes.list_id) && {
entries: (doc.attributes.entries as EntriesArray).map<EntryType>(migrateEntry),
}),
...(doc.attributes._tags &&
doc.attributes._tags.reduce(reduceOsTypes, []).length > 0 && {
os_types: doc.attributes._tags.reduce(reduceOsTypes, []) as OsTypeArray,
}),
},
},
references: doc.references || [],
}),
};

View file

@ -1,10 +1,10 @@
{
"item_id": "simple_list_item",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample endpoint type exception",
"name": "Sample Endpoint Exception List",
"os_types": ["linux"],
"entries": [
{
"field": "actingProcess.file.signer",

View file

@ -1,6 +1,5 @@
{
"list_id": "simple_list",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "detection",
"description": "This is a sample endpoint type exception",

View file

@ -1,6 +1,5 @@
{
"list_id": "endpoint_list",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "endpoint",
"description": "This is a sample agnostic endpoint type exception",

View file

@ -1,6 +1,5 @@
{
"list_id": "detection_list",
"_tags": ["detection"],
"tags": ["detection", "sample_tag"],
"type": "detection",
"description": "This is a sample detection type exception list",

View file

@ -1,11 +1,11 @@
{
"list_id": "simple_list",
"item_id": "simple_list_item",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample endpoint type exception",
"name": "Sample Endpoint Exception List",
"os_types": ["linux"],
"entries": [
{
"field": "actingProcess.file.signer",

View file

@ -1,12 +1,12 @@
{
"list_id": "endpoint_list",
"item_id": "endpoint_list_item",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample agnostic endpoint type exception",
"name": "Sample Endpoint Exception List",
"namespace_type": "agnostic",
"os_types": ["linux"],
"entries": [
{
"field": "actingProcess.file.signer",

View file

@ -1,10 +1,10 @@
{
"list_id": "simple_list",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample endpoint type exception that has no item_id so it creates a new id each time",
"name": "Sample Endpoint Exception List",
"os_types": ["linux"],
"comments": [],
"entries": [
{

View file

@ -1,6 +1,5 @@
{
"list_id": "detection_list",
"_tags": ["detection"],
"tags": ["test_tag", "detection", "no_more_bad_guys"],
"type": "simple",
"description": "This is a sample detection type exception that has no item_id so it creates a new id each time",

View file

@ -1,11 +1,11 @@
{
"list_id": "endpoint_list",
"item_id": "endpoint_list_item_good_rock01",
"_tags": ["endpoint", "process", "malware", "os:windows"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "Don't signal when agent.name is rock01 and source.ip is in the goodguys.txt list",
"name": "Filter out good guys ip and agent.name rock01",
"os_types": ["windows"],
"comments": [],
"entries": [
{

View file

@ -1,11 +1,11 @@
{
"list_id": "endpoint_list",
"item_id": "endpoint_list_item_lg_val_list",
"_tags": ["endpoint", "process", "malware", "os:windows"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample exception list item with a large value list included",
"name": "Sample Endpoint Exception List Item with large value list",
"os_types": ["windows"],
"comments": [],
"entries": [
{

View file

@ -1,12 +1,12 @@
{
"list_id": "endpoint_trusted_apps",
"item_id": "endpoint_trusted_apps_item",
"_tags": ["endpoint", "os:linux", "os:windows", "os:macos", "trusted-app"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample agnostic endpoint trusted app entry",
"name": "Sample Endpoint Trusted App Entry",
"namespace_type": "agnostic",
"os_types": ["linux", "windows", "macos"],
"entries": [
{
"field": "actingProcess.file.signer",

View file

@ -1,8 +1,8 @@
{
"list_id": "simple_list",
"_tags": ["endpoint", "process", "malware", "os:linux"],
"tags": ["user added string for a tag", "malware"],
"type": "endpoint",
"os_types": ["linux"],
"description": "Different description",
"name": "Sample Endpoint Exception List"
}

View file

@ -1,11 +1,11 @@
{
"item_id": "endpoint_list_item",
"_tags": ["endpoint", "process", "malware", "os:windows"],
"tags": ["user added string for a tag", "malware"],
"type": "simple",
"description": "This is a sample agnostic change here this list",
"name": "Sample Endpoint Exception List update change",
"namespace_type": "agnostic",
"os_types": ["windows"],
"entries": [
{
"field": "event.category",

View file

@ -1,5 +1,4 @@
{
"_tags": ["detection"],
"comments": [],
"description": "Test comments - exception list item",
"entries": [

View file

@ -35,7 +35,6 @@ export const createEndpointList = async ({
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(
savedObjectType,
{
_tags: [],
comments: undefined,
created_at: dateNow,
created_by: user,
@ -47,6 +46,7 @@ export const createEndpointList = async ({
list_type: 'list',
meta: undefined,
name: ENDPOINT_LIST_NAME,
os_types: [],
tags: [],
tie_breaker_id: tieBreaker ?? uuid.v4(),
type: 'endpoint',

View file

@ -43,7 +43,6 @@ export const createEndpointTrustedAppsList = async ({
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(
savedObjectType,
{
_tags: [],
comments: undefined,
created_at: dateNow,
created_by: user,
@ -55,6 +54,7 @@ export const createEndpointTrustedAppsList = async ({
list_type: 'list',
meta: undefined,
name: ENDPOINT_TRUSTED_APPS_LIST_NAME,
os_types: [],
tags: [],
tie_breaker_id: tieBreaker ?? uuid.v4(),
type: 'endpoint',

View file

@ -19,13 +19,11 @@ import {
NamespaceType,
Tags,
Version,
_Tags,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils';
interface CreateExceptionListOptions {
_tags: _Tags;
listId: ListId;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
@ -41,7 +39,6 @@ interface CreateExceptionListOptions {
}
export const createExceptionList = async ({
_tags,
listId,
immutable,
savedObjectsClient,
@ -58,7 +55,6 @@ export const createExceptionList = async ({
const savedObjectType = getSavedObjectType({ namespaceType });
const dateNow = new Date().toISOString();
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(savedObjectType, {
_tags,
comments: undefined,
created_at: dateNow,
created_by: user,
@ -70,6 +66,7 @@ export const createExceptionList = async ({
list_type: 'list',
meta,
name,
os_types: [],
tags,
tie_breaker_id: tieBreaker ?? uuid.v4(),
type,

View file

@ -19,8 +19,8 @@ import {
MetaOrUndefined,
Name,
NamespaceType,
OsTypeArray,
Tags,
_Tags,
} from '../../../common/schemas';
import {
@ -30,7 +30,6 @@ import {
} from './utils';
interface CreateExceptionListItemOptions {
_tags: _Tags;
comments: CreateCommentsArray;
listId: ListId;
itemId: ItemId;
@ -44,10 +43,10 @@ interface CreateExceptionListItemOptions {
tags: Tags;
tieBreaker?: string;
type: ExceptionListItemType;
osTypes: OsTypeArray;
}
export const createExceptionListItem = async ({
_tags,
comments,
entries,
itemId,
@ -55,6 +54,7 @@ export const createExceptionListItem = async ({
savedObjectsClient,
namespaceType,
name,
osTypes,
description,
meta,
user,
@ -69,7 +69,6 @@ export const createExceptionListItem = async ({
user,
});
const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(savedObjectType, {
_tags,
comments: transformedComments,
created_at: dateNow,
created_by: user,
@ -81,6 +80,7 @@ export const createExceptionListItem = async ({
list_type: 'item',
meta,
name,
os_types: osTypes as OsTypeArray,
tags,
tie_breaker_id: tieBreaker ?? uuid.v4(),
type,

View file

@ -109,20 +109,19 @@ export class ExceptionListClient {
* being there and existing before the item is inserted into the agnostic endpoint list.
*/
public createEndpointListItem = async ({
_tags,
comments,
description,
entries,
itemId,
meta,
name,
osTypes,
tags,
type,
}: CreateEndpointListItemOptions): Promise<ExceptionListItemSchema> => {
const { savedObjectsClient, user } = this;
await this.createEndpointList();
return createExceptionListItem({
_tags,
comments,
description,
entries,
@ -131,6 +130,7 @@ export class ExceptionListClient {
meta,
name,
namespaceType: 'agnostic',
osTypes,
savedObjectsClient,
tags,
type,
@ -145,7 +145,6 @@ export class ExceptionListClient {
* return of null but at least the list exists again.
*/
public updateEndpointListItem = async ({
_tags,
_version,
comments,
description,
@ -154,13 +153,13 @@ export class ExceptionListClient {
itemId,
meta,
name,
osTypes,
tags,
type,
}: UpdateEndpointListItemOptions): Promise<ExceptionListItemSchema | null> => {
const { savedObjectsClient, user } = this;
await this.createEndpointList();
return updateExceptionListItem({
_tags,
_version,
comments,
description,
@ -170,6 +169,7 @@ export class ExceptionListClient {
meta,
name,
namespaceType: 'agnostic',
osTypes,
savedObjectsClient,
tags,
type,
@ -189,7 +189,6 @@ export class ExceptionListClient {
};
public createExceptionList = async ({
_tags,
description,
immutable,
listId,
@ -202,7 +201,6 @@ export class ExceptionListClient {
}: CreateExceptionListOptions): Promise<ExceptionListSchema> => {
const { savedObjectsClient, user } = this;
return createExceptionList({
_tags,
description,
immutable,
listId,
@ -218,7 +216,6 @@ export class ExceptionListClient {
};
public updateExceptionList = async ({
_tags,
_version,
id,
description,
@ -226,13 +223,13 @@ export class ExceptionListClient {
meta,
name,
namespaceType,
osTypes,
tags,
type,
version,
}: UpdateExceptionListOptions): Promise<ExceptionListSchema | null> => {
const { savedObjectsClient, user } = this;
return updateExceptionList({
_tags,
_version,
description,
id,
@ -240,6 +237,7 @@ export class ExceptionListClient {
meta,
name,
namespaceType,
osTypes,
savedObjectsClient,
tags,
type,
@ -263,7 +261,6 @@ export class ExceptionListClient {
};
public createExceptionListItem = async ({
_tags,
comments,
description,
entries,
@ -272,12 +269,12 @@ export class ExceptionListClient {
meta,
name,
namespaceType,
osTypes,
tags,
type,
}: CreateExceptionListItemOptions): Promise<ExceptionListItemSchema> => {
const { savedObjectsClient, user } = this;
return createExceptionListItem({
_tags,
comments,
description,
entries,
@ -286,6 +283,7 @@ export class ExceptionListClient {
meta,
name,
namespaceType,
osTypes,
savedObjectsClient,
tags,
type,
@ -294,7 +292,6 @@ export class ExceptionListClient {
};
public updateExceptionListItem = async ({
_tags,
_version,
comments,
description,
@ -304,12 +301,12 @@ export class ExceptionListClient {
meta,
name,
namespaceType,
osTypes,
tags,
type,
}: UpdateExceptionListItemOptions): Promise<ExceptionListItemSchema | null> => {
const { savedObjectsClient, user } = this;
return updateExceptionListItem({
_tags,
_version,
comments,
description,
@ -319,6 +316,7 @@ export class ExceptionListClient {
meta,
name,
namespaceType,
osTypes,
savedObjectsClient,
tags,
type,

View file

@ -30,6 +30,7 @@ import {
Name,
NameOrUndefined,
NamespaceType,
OsTypeArray,
PageOrUndefined,
PerPageOrUndefined,
SortFieldOrUndefined,
@ -39,8 +40,6 @@ import {
UpdateCommentsArray,
Version,
VersionOrUndefined,
_Tags,
_TagsOrUndefined,
_VersionOrUndefined,
} from '../../../common/schemas';
@ -56,7 +55,6 @@ export interface GetExceptionListOptions {
}
export interface CreateExceptionListOptions {
_tags: _Tags;
listId: ListId;
namespaceType: NamespaceType;
name: Name;
@ -69,12 +67,12 @@ export interface CreateExceptionListOptions {
}
export interface UpdateExceptionListOptions {
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
id: IdOrUndefined;
listId: ListIdOrUndefined;
namespaceType: NamespaceType;
name: NameOrUndefined;
osTypes: OsTypeArray;
description: DescriptionOrUndefined;
meta: MetaOrUndefined;
tags: TagsOrUndefined;
@ -116,13 +114,13 @@ export interface GetEndpointListItemOptions {
}
export interface CreateExceptionListItemOptions {
_tags: _Tags;
comments: CreateCommentsArray;
entries: EntriesArray;
itemId: ItemId;
listId: ListId;
namespaceType: NamespaceType;
name: Name;
osTypes: OsTypeArray;
description: Description;
meta: MetaOrUndefined;
tags: Tags;
@ -130,19 +128,18 @@ export interface CreateExceptionListItemOptions {
}
export interface CreateEndpointListItemOptions {
_tags: _Tags;
comments: CreateCommentsArray;
entries: EntriesArray;
itemId: ItemId;
name: Name;
description: Description;
meta: MetaOrUndefined;
osTypes: OsTypeArray;
tags: Tags;
type: ExceptionListItemType;
}
export interface UpdateExceptionListItemOptions {
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
comments: UpdateCommentsArray;
entries: EntriesArray;
@ -150,6 +147,7 @@ export interface UpdateExceptionListItemOptions {
itemId: ItemIdOrUndefined;
namespaceType: NamespaceType;
name: NameOrUndefined;
osTypes: OsTypeArray;
description: DescriptionOrUndefined;
meta: MetaOrUndefined;
tags: TagsOrUndefined;
@ -157,13 +155,13 @@ export interface UpdateExceptionListItemOptions {
}
export interface UpdateEndpointListItemOptions {
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
comments: UpdateCommentsArray;
entries: EntriesArray;
id: IdOrUndefined;
itemId: ItemIdOrUndefined;
name: NameOrUndefined;
osTypes: OsTypeArray;
description: DescriptionOrUndefined;
meta: MetaOrUndefined;
tags: TagsOrUndefined;

View file

@ -16,9 +16,9 @@ import {
MetaOrUndefined,
NameOrUndefined,
NamespaceType,
OsTypeArray,
TagsOrUndefined,
VersionOrUndefined,
_TagsOrUndefined,
_VersionOrUndefined,
} from '../../../common/schemas';
@ -27,12 +27,12 @@ import { getExceptionList } from './get_exception_list';
interface UpdateExceptionListOptions {
id: IdOrUndefined;
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
name: NameOrUndefined;
description: DescriptionOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
osTypes: OsTypeArray;
listId: ListIdOrUndefined;
meta: MetaOrUndefined;
user: string;
@ -43,7 +43,6 @@ interface UpdateExceptionListOptions {
}
export const updateExceptionList = async ({
_tags,
_version,
id,
savedObjectsClient,
@ -67,7 +66,6 @@ export const updateExceptionList = async ({
savedObjectType,
exceptionList.id,
{
_tags,
description,
meta,
name,

View file

@ -17,9 +17,9 @@ import {
MetaOrUndefined,
NameOrUndefined,
NamespaceType,
OsTypeArray,
TagsOrUndefined,
UpdateCommentsArrayOrUndefined,
_TagsOrUndefined,
_VersionOrUndefined,
} from '../../../common/schemas';
@ -33,13 +33,13 @@ import { getExceptionListItem } from './get_exception_list_item';
interface UpdateExceptionListItemOptions {
id: IdOrUndefined;
comments: UpdateCommentsArrayOrUndefined;
_tags: _TagsOrUndefined;
_version: _VersionOrUndefined;
name: NameOrUndefined;
description: DescriptionOrUndefined;
entries: EntriesArray;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
osTypes: OsTypeArray;
itemId: ItemIdOrUndefined;
meta: MetaOrUndefined;
user: string;
@ -49,7 +49,6 @@ interface UpdateExceptionListItemOptions {
}
export const updateExceptionListItem = async ({
_tags,
_version,
comments,
entries,
@ -57,6 +56,7 @@ export const updateExceptionListItem = async ({
savedObjectsClient,
namespaceType,
name,
osTypes,
description,
itemId,
meta,
@ -83,12 +83,12 @@ export const updateExceptionListItem = async ({
savedObjectType,
exceptionListItem.id,
{
_tags,
comments: transformedComments,
description,
entries,
meta,
name,
os_types: osTypes,
tags,
type,
updated_by: user,

View file

@ -71,7 +71,6 @@ export const transformSavedObjectToExceptionList = ({
version: _version,
attributes: {
/* eslint-disable @typescript-eslint/naming-convention */
_tags,
created_at,
created_by,
description,
@ -79,6 +78,7 @@ export const transformSavedObjectToExceptionList = ({
list_id,
meta,
name,
os_types,
tags,
tie_breaker_id,
type,
@ -93,7 +93,6 @@ export const transformSavedObjectToExceptionList = ({
// TODO: Change this to do a decode and throw if the saved object is not as expected.
// TODO: Do a throw if after the decode this is not the correct "list_type: list"
return {
_tags,
_version,
created_at,
created_by,
@ -104,6 +103,7 @@ export const transformSavedObjectToExceptionList = ({
meta,
name,
namespace_type: getExceptionListType({ savedObjectType: savedObject.type }),
os_types,
tags,
tie_breaker_id,
type: exceptionListType.is(type) ? type : 'detection',
@ -124,11 +124,11 @@ export const transformSavedObjectUpdateToExceptionList = ({
const {
version: _version,
attributes: {
_tags,
description,
immutable,
meta,
name,
os_types: osTypes,
tags,
type,
updated_by: updatedBy,
@ -141,7 +141,6 @@ export const transformSavedObjectUpdateToExceptionList = ({
// TODO: Change this to do a decode and throw if the saved object is not as expected.
// TODO: Do a throw if after the decode this is not the correct "list_type: list"
return {
_tags: _tags ?? exceptionList._tags,
_version,
created_at: exceptionList.created_at,
created_by: exceptionList.created_by,
@ -152,6 +151,7 @@ export const transformSavedObjectUpdateToExceptionList = ({
meta: meta ?? exceptionList.meta,
name: name ?? exceptionList.name,
namespace_type: getExceptionListType({ savedObjectType: savedObject.type }),
os_types: osTypes ?? exceptionList.os_types,
tags: tags ?? exceptionList.tags,
tie_breaker_id: exceptionList.tie_breaker_id,
type: exceptionListType.is(type) ? type : exceptionList.type,
@ -171,7 +171,6 @@ export const transformSavedObjectToExceptionListItem = ({
version: _version,
attributes: {
/* eslint-disable @typescript-eslint/naming-convention */
_tags,
comments,
created_at,
created_by,
@ -181,6 +180,7 @@ export const transformSavedObjectToExceptionListItem = ({
list_id,
meta,
name,
os_types,
tags,
tie_breaker_id,
type,
@ -194,7 +194,6 @@ export const transformSavedObjectToExceptionListItem = ({
// TODO: Do a throw if after the decode this is not the correct "list_type: item"
// TODO: Do a throw if item_id or entries is not defined.
return {
_tags,
_version,
comments: comments ?? [],
created_at,
@ -207,6 +206,7 @@ export const transformSavedObjectToExceptionListItem = ({
meta,
name,
namespace_type: getExceptionListType({ savedObjectType: savedObject.type }),
os_types,
tags,
tie_breaker_id,
type: exceptionListItemType.is(type) ? type : 'simple',
@ -226,12 +226,12 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
const {
version: _version,
attributes: {
_tags,
comments,
description,
entries,
meta,
name,
os_types: osTypes,
tags,
type,
updated_by: updatedBy,
@ -245,7 +245,6 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
// TODO: Update exception list and item types (perhaps separating out) so as to avoid
// defaulting
return {
_tags: _tags ?? exceptionListItem._tags,
_version,
comments: comments ?? exceptionListItem.comments,
created_at: exceptionListItem.created_at,
@ -258,6 +257,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
meta: meta ?? exceptionListItem.meta,
name: name ?? exceptionListItem.name,
namespace_type: getExceptionListType({ savedObjectType: savedObject.type }),
os_types: osTypes ?? exceptionListItem.os_types,
tags: tags ?? exceptionListItem.tags,
tie_breaker_id: exceptionListItem.tie_breaker_id,
type: exceptionListItemType.is(type) ? type : exceptionListItem.type,

View file

@ -0,0 +1,80 @@
/*
* 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 { DefaultArray } from './default_array';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
const testSchema = t.keyof({
valid: true,
also_valid: true,
});
type TestSchema = t.TypeOf<typeof testSchema>;
const defaultArraySchema = DefaultArray(testSchema);
describe('default_array', () => {
test('it should validate an empty array', () => {
const payload: string[] = [];
const decoded = defaultArraySchema.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 testSchema', () => {
const payload: TestSchema[] = ['valid'];
const decoded = defaultArraySchema.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 valid testSchema strings', () => {
const payload = ['valid', 'also_valid'];
const decoded = defaultArraySchema.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 with a number', () => {
const payload = ['valid', 123];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "123" supplied to "DefaultArray"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate an array with an invalid string', () => {
const payload = ['valid', 'invalid'];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "invalid" supplied to "DefaultArray"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,25 @@
/*
* 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 DefaultArray<C> as:
* - If undefined, then a default array will be set
* - If an array is sent in, then the array will be validated to ensure all elements are type C
*/
export const DefaultArray = <C extends t.Mixed>(codec: C) => {
const arrType = t.array(codec);
type ArrType = t.TypeOf<typeof arrType>;
return new t.Type<ArrType, ArrType | undefined, unknown>(
'DefaultArray',
arrType.is,
(input, context): Either<t.Errors, ArrType> =>
input == null ? t.success([]) : arrType.validate(input, context),
t.identity
);
};

View file

@ -5,6 +5,7 @@
*/
export * from './default_actions_array';
export * from './default_array';
export * from './default_boolean_false';
export * from './default_boolean_true';
export * from './default_empty_string';

View file

@ -76,7 +76,7 @@ describe('When invoking Trusted Apps Schema', () => {
os: 'windows',
entries: [
{
field: 'process.executable.text',
field: 'process.executable.caseless',
type: 'match',
operator: 'included',
value: 'c:/programs files/Anti-Virus',
@ -204,7 +204,7 @@ describe('When invoking Trusted Apps Schema', () => {
field: 'process.hash.*',
value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
{ field: 'process.executable.text', value: '/tmp/dir1' },
{ field: 'process.executable.caseless', value: '/tmp/dir1' },
].forEach((partialEntry) => {
const bodyMsg3 = {
...getCreateTrustedAppItem(),

View file

@ -35,7 +35,7 @@ export const PostTrustedAppCreateRequestSchema = {
schema.object({
field: schema.oneOf([
schema.literal('process.hash.*'),
schema.literal('process.executable.text'),
schema.literal('process.executable.caseless'),
]),
type: schema.literal('match'),
operator: schema.literal('included'),

View file

@ -33,7 +33,7 @@ export interface PostTrustedAppCreateResponse {
}
export interface MacosLinuxConditionEntry {
field: 'process.hash.*' | 'process.executable.text';
field: 'process.hash.*' | 'process.executable.caseless';
type: 'match';
operator: 'included';
value: string;

View file

@ -5,6 +5,7 @@
*/
export { NonEmptyString } from './detection_engine/schemas/types/non_empty_string';
export { DefaultArray } from './detection_engine/schemas/types/default_array';
export { DefaultUuid } from './detection_engine/schemas/types/default_uuid';
export { DefaultStringArray } from './detection_engine/schemas/types/default_string_array';
export {

View file

@ -42,4 +42,6 @@ export {
ExceptionListType,
Type,
ENDPOINT_LIST_ID,
osTypeArray,
OsTypeArray,
} from '../../lists/common';

View file

@ -257,7 +257,7 @@ describe('When the add exception modal is opened', () => {
indexPatterns: {
...stubIndexPattern,
fields: [
{ name: 'file.path.text', type: 'string' },
{ name: 'file.path.caseless', type: 'string' },
{ name: 'subject_name', type: 'string' },
{ name: 'trusted', type: 'string' },
{ name: 'file.hash.sha256', type: 'string' },

View file

@ -30,6 +30,7 @@ import * as i18nCommon from '../../../translations';
import * as i18n from './translations';
import * as sharedI18n from '../translations';
import { Ecs } from '../../../../../common/ecs';
import { osTypeArray, OsTypeArray } from '../../../../../common/shared_imports';
import { useAppToasts } from '../../../hooks/use_app_toasts';
import { useKibana } from '../../../lib/kibana';
import { ExceptionBuilderComponent } from '../builder';
@ -211,12 +212,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
const initialExceptionItems = useMemo((): ExceptionsBuilderExceptionItem[] => {
if (exceptionListType === 'endpoint' && alertData != null && ruleExceptionList) {
return defaultEndpointExceptionItems(
exceptionListType,
ruleExceptionList.list_id,
ruleName,
alertData
);
return defaultEndpointExceptionItems(ruleExceptionList.list_id, ruleName, alertData);
} else {
return [];
}
@ -265,11 +261,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({
[setShouldBulkCloseAlert]
);
const retrieveAlertOsTypes = useCallback((): string[] => {
const osDefaults = ['windows', 'macos'];
const retrieveAlertOsTypes = useCallback((): OsTypeArray => {
const osDefaults: OsTypeArray = ['windows', 'macos'];
if (alertData != null) {
const osTypes = alertData.host && alertData.host.os && alertData.host.os.family;
if (osTypes != null && osTypes.length > 0) {
if (osTypeArray.is(osTypes) && osTypes != null && osTypes.length > 0) {
return osTypes;
}
return osDefaults;
@ -316,13 +312,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({
[fetchOrCreateListError, exceptionItemsToAdd]
);
const addExceptionMessage =
exceptionListType === 'endpoint' ? i18n.ADD_ENDPOINT_EXCEPTION : i18n.ADD_EXCEPTION;
return (
<EuiOverlayMask onClick={onCancel}>
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
<ModalHeader>
<EuiModalHeaderTitle>
{exceptionListType === 'endpoint' ? i18n.ADD_ENDPOINT_EXCEPTION : i18n.ADD_EXCEPTION}
</EuiModalHeaderTitle>
<EuiModalHeaderTitle>{addExceptionMessage}</EuiModalHeaderTitle>
<ModalHeaderSubtitle className="eui-textTruncate" title={ruleName}>
{ruleName}
</ModalHeaderSubtitle>
@ -429,7 +426,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
isDisabled={isSubmitButtonDisabled}
fill
>
{i18n.ADD_EXCEPTION}
{addExceptionMessage}
</EuiButton>
</EuiModalFooter>
)}

View file

@ -90,9 +90,9 @@ const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({
const mockEndpointFields = [
{
name: 'file.path.text',
name: 'file.path.caseless',
type: 'string',
esTypes: ['text'],
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
@ -303,8 +303,8 @@ describe('Exception builder helpers', () => {
{
aggregatable: false,
count: 0,
esTypes: ['text'],
name: 'file.path.text',
esTypes: ['keyword'],
name: 'file.path.caseless',
readFromDocValues: false,
scripted: false,
searchable: true,

View file

@ -234,13 +234,12 @@ export const ExceptionBuilderComponent = ({
// empty `entries` array. Thought about appending an entry item to one, but that
// would then be arbitrary, decided to just create a new exception list item
const newException = getNewExceptionItem({
listType,
listId,
namespaceType: listNamespaceType,
ruleName,
});
setUpdateExceptions([...exceptions, { ...newException }]);
}, [setUpdateExceptions, exceptions, listType, listId, listNamespaceType, ruleName]);
}, [setUpdateExceptions, exceptions, listId, listNamespaceType, ruleName]);
// The builder can have existing exception items, or new exception items that have yet
// to be created (and thus lack an id), this was creating some React bugs with relying

View file

@ -40,7 +40,6 @@ import { AddExceptionComments } from '../add_exception_comments';
import {
enrichExistingExceptionItemWithComments,
enrichExceptionItemsWithOS,
getOperatingSystems,
entryHasListType,
entryHasNonEcsType,
lowercaseHashValues,
@ -228,8 +227,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
},
];
if (exceptionListType === 'endpoint') {
const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : [];
enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes));
enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, exceptionItem.os_types));
}
return enriched;
}, [exceptionItemsToAdd, exceptionItem, comment, exceptionListType]);

View file

@ -6,33 +6,33 @@
"Target.process.Ext.code_signature.valid",
"Target.process.Ext.services",
"Target.process.Ext.user",
"Target.process.command_line.text",
"Target.process.executable.text",
"Target.process.command_line.caseless",
"Target.process.executable.caseless",
"Target.process.hash.md5",
"Target.process.hash.sha1",
"Target.process.hash.sha256",
"Target.process.hash.sha512",
"Target.process.name.text",
"Target.process.name.caseless",
"Target.process.parent.Ext.code_signature.status",
"Target.process.parent.Ext.code_signature.subject_name",
"Target.process.parent.Ext.code_signature.trusted",
"Target.process.parent.Ext.code_signature.valid",
"Target.process.parent.command_line.text",
"Target.process.parent.executable.text",
"Target.process.parent.command_line.caseless",
"Target.process.parent.executable.caseless",
"Target.process.parent.hash.md5",
"Target.process.parent.hash.sha1",
"Target.process.parent.hash.sha256",
"Target.process.parent.hash.sha512",
"Target.process.parent.name.text",
"Target.process.parent.name.caseless",
"Target.process.parent.pgid",
"Target.process.parent.working_directory.text",
"Target.process.parent.working_directory.caseless",
"Target.process.pe.company",
"Target.process.pe.description",
"Target.process.pe.file_version",
"Target.process.pe.original_file_name",
"Target.process.pe.product",
"Target.process.pgid",
"Target.process.working_directory.text",
"Target.process.working_directory.caseless",
"agent.id",
"agent.type",
"agent.version",
@ -66,14 +66,14 @@
"file.mode",
"file.name",
"file.owner",
"file.path.text",
"file.path.caseless",
"file.pe.company",
"file.pe.description",
"file.pe.file_version",
"file.pe.original_file_name",
"file.pe.product",
"file.size",
"file.target_path.text",
"file.target_path.caseless",
"file.type",
"file.uid",
"group.Ext.real.id",
@ -84,9 +84,9 @@
"host.id",
"host.os.Ext.variant",
"host.os.family",
"host.os.full.text",
"host.os.full.caseless",
"host.os.kernel",
"host.os.name.text",
"host.os.name.caseless",
"host.os.platform",
"host.os.version",
"host.type",
@ -96,33 +96,33 @@
"process.Ext.code_signature.valid",
"process.Ext.services",
"process.Ext.user",
"process.command_line.text",
"process.executable.text",
"process.command_line.caseless",
"process.executable.caseless",
"process.hash.md5",
"process.hash.sha1",
"process.hash.sha256",
"process.hash.sha512",
"process.name.text",
"process.name.caseless",
"process.parent.Ext.code_signature.status",
"process.parent.Ext.code_signature.subject_name",
"process.parent.Ext.code_signature.trusted",
"process.parent.Ext.code_signature.valid",
"process.parent.command_line.text",
"process.parent.executable.text",
"process.parent.command_line.caseless",
"process.parent.executable.caseless",
"process.parent.hash.md5",
"process.parent.hash.sha1",
"process.parent.hash.sha256",
"process.parent.hash.sha512",
"process.parent.name.text",
"process.parent.name.caseless",
"process.parent.pgid",
"process.parent.working_directory.text",
"process.parent.working_directory.caseless",
"process.pe.company",
"process.pe.description",
"process.pe.file_version",
"process.pe.original_file_name",
"process.pe.product",
"process.pgid",
"process.working_directory.text",
"process.working_directory.caseless",
"rule.uuid",
"user.domain",
"user.email",

View file

@ -10,8 +10,6 @@ import moment from 'moment-timezone';
import {
getOperatorType,
getExceptionOperatorSelect,
getOperatingSystems,
getTagsInclude,
getFormattedComments,
filterExceptionItems,
getNewExceptionItem,
@ -52,6 +50,7 @@ import {
CreateExceptionListItemSchema,
ExceptionListItemSchema,
EntriesArray,
OsTypeArray,
} from '../../../../../lists/common/schemas';
import { IIndexPattern } from 'src/plugins/data/common';
@ -186,76 +185,18 @@ describe('Exception helpers', () => {
});
});
describe('#getOperatingSystems', () => {
test('it returns null if no operating system tag specified', () => {
const result = getOperatingSystems(['some tag', 'some other tag']);
expect(result).toEqual([]);
});
test('it returns null if operating system tag malformed', () => {
const result = getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag']);
expect(result).toEqual([]);
});
test('it returns operating systems if space included in os tag', () => {
const result = getOperatingSystems(['some tag', 'os: macos', 'some other tag']);
expect(result).toEqual(['macos']);
});
test('it returns operating systems if multiple os tags specified', () => {
const result = getOperatingSystems(['some tag', 'os: macos', 'some other tag', 'os:windows']);
expect(result).toEqual(['macos', 'windows']);
});
});
describe('#formatOperatingSystems', () => {
test('it returns null if no operating system tag specified', () => {
const result = formatOperatingSystems(getOperatingSystems(['some tag', 'some other tag']));
const result = formatOperatingSystems(['some os', 'some other os']);
expect(result).toEqual('');
});
test('it returns null if operating system tag malformed', () => {
const result = formatOperatingSystems(
getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag'])
);
expect(result).toEqual('');
});
test('it returns formatted operating systems if space included in os tag', () => {
const result = formatOperatingSystems(
getOperatingSystems(['some tag', 'os: macos', 'some other tag'])
);
expect(result).toEqual('macOS');
});
test('it returns formatted operating systems if multiple os tags specified', () => {
const result = formatOperatingSystems(
getOperatingSystems(['some tag', 'os: macos', 'some other tag', 'os:windows'])
);
test('it returns formatted operating systems if multiple specified', () => {
const result = formatOperatingSystems(['some tag', 'macos', 'some other tag', 'windows']);
expect(result).toEqual('macOS, Windows');
});
});
describe('#getTagsInclude', () => {
test('it returns a tuple of "false" and "null" if no matches found', () => {
const result = getTagsInclude({ tags: ['some', 'tags', 'here'], regex: /(no match)/ });
expect(result).toEqual([false, null]);
});
test('it returns a tuple of "true" and matching string if matches found', () => {
const result = getTagsInclude({ tags: ['some', 'tags', 'here'], regex: /(some)/ });
expect(result).toEqual([true, 'some']);
});
});
describe('#getFormattedComments', () => {
test('it returns formatted comment object with username and timestamp', () => {
const payload = getCommentsArrayMock();
@ -384,7 +325,6 @@ describe('Exception helpers', () => {
test('it removes `temporaryId` from items', () => {
const { meta, ...rest } = getNewExceptionItem({
listType: 'detection',
listId: '123',
namespaceType: 'single',
ruleName: 'rule name',
@ -400,7 +340,6 @@ describe('Exception helpers', () => {
const payload = getExceptionListItemSchemaMock();
const result = formatExceptionItemForUpdate(payload);
const expected = {
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
comments: [],
description: 'some description',
entries: ENTRIES,
@ -409,6 +348,7 @@ describe('Exception helpers', () => {
meta: {},
name: 'some name',
namespace_type: 'single',
os_types: ['linux'],
tags: ['user added string for a tag', 'malware'],
type: 'simple',
};
@ -489,14 +429,14 @@ describe('Exception helpers', () => {
});
describe('#enrichExceptionItemsWithOS', () => {
test('it should add an os tag to an exception item', () => {
test('it should add an os to an exception item', () => {
const payload = [getExceptionListItemSchemaMock()];
const osTypes = ['windows'];
const osTypes: OsTypeArray = ['windows'];
const result = enrichExceptionItemsWithOS(payload, osTypes);
const expected = [
{
...getExceptionListItemSchemaMock(),
_tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows'],
os_types: ['windows'],
},
];
expect(result).toEqual(expected);
@ -504,36 +444,16 @@ describe('Exception helpers', () => {
test('it should add multiple os tags to all exception items', () => {
const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
const osTypes = ['windows', 'macos'];
const osTypes: OsTypeArray = ['windows', 'macos'];
const result = enrichExceptionItemsWithOS(payload, osTypes);
const expected = [
{
...getExceptionListItemSchemaMock(),
_tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows', 'os:macos'],
os_types: ['windows', 'macos'],
},
{
...getExceptionListItemSchemaMock(),
_tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows', 'os:macos'],
},
];
expect(result).toEqual(expected);
});
test('it should add os tag to all exception items without duplication', () => {
const payload = [
{ ...getExceptionListItemSchemaMock(), _tags: ['os:linux', 'os:windows'] },
{ ...getExceptionListItemSchemaMock(), _tags: ['os:linux'] },
];
const osTypes = ['windows'];
const result = enrichExceptionItemsWithOS(payload, osTypes);
const expected = [
{
...getExceptionListItemSchemaMock(),
_tags: ['os:linux', 'os:windows'],
},
{
...getExceptionListItemSchemaMock(),
_tags: ['os:linux', 'os:windows'],
os_types: ['windows', 'macos'],
},
];
expect(result).toEqual(expected);
@ -715,7 +635,6 @@ describe('Exception helpers', () => {
describe('getPrepopulatedItem', () => {
test('it returns prepopulated items', () => {
const prepopulatedItem = getPrepopulatedItem({
listType: 'endpoint',
listId: 'some_id',
ruleName: 'my rule',
codeSignature: { subjectName: '', trusted: '' },
@ -733,7 +652,7 @@ describe('Exception helpers', () => {
field: 'file.Ext.code_signature',
type: 'nested',
},
{ field: 'file.path.text', operator: 'included', type: 'match', value: '' },
{ field: 'file.path.caseless', operator: 'included', type: 'match', value: '' },
{ field: 'file.hash.sha256', operator: 'included', type: 'match', value: '' },
{ field: 'event.code', operator: 'included', type: 'match', value: '' },
]);
@ -741,7 +660,6 @@ describe('Exception helpers', () => {
test('it returns prepopulated items with values', () => {
const prepopulatedItem = getPrepopulatedItem({
listType: 'endpoint',
listId: 'some_id',
ruleName: 'my rule',
codeSignature: { subjectName: 'someSubjectName', trusted: 'false' },
@ -764,7 +682,12 @@ describe('Exception helpers', () => {
field: 'file.Ext.code_signature',
type: 'nested',
},
{ field: 'file.path.text', operator: 'included', type: 'match', value: 'some-file-path' },
{
field: 'file.path.caseless',
operator: 'included',
type: 'match',
value: 'some-file-path',
},
{ field: 'file.hash.sha256', operator: 'included', type: 'match', value: 'some-hash' },
{ field: 'event.code', operator: 'included', type: 'match', value: 'some-event-code' },
]);
@ -847,7 +770,7 @@ describe('Exception helpers', () => {
describe('defaultEndpointExceptionItems', () => {
test('it should return pre-populated items', () => {
const defaultItems = defaultEndpointExceptionItems('endpoint', 'list_id', 'my_rule', {
const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', {
_id: '123',
file: {
Ext: {
@ -881,7 +804,7 @@ describe('Exception helpers', () => {
type: 'nested',
},
{
field: 'file.path.text',
field: 'file.path.caseless',
operator: 'included',
type: 'match',
value: 'some file path',
@ -904,7 +827,7 @@ describe('Exception helpers', () => {
type: 'nested',
},
{
field: 'file.path.text',
field: 'file.path.caseless',
operator: 'included',
type: 'match',
value: 'some file path',

View file

@ -6,7 +6,7 @@
import React from 'react';
import { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui';
import { capitalize, union } from 'lodash';
import { capitalize } from 'lodash';
import moment from 'moment';
import uuid from 'uuid';
@ -33,8 +33,8 @@ import {
createExceptionListItemSchema,
exceptionListItemSchema,
UpdateExceptionListItemSchema,
ExceptionListType,
EntryNested,
OsTypeArray,
} from '../../../shared_imports';
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { validate } from '../../../../common/validate';
@ -98,20 +98,12 @@ export const getEntryValue = (item: BuilderEntry): string | string[] | undefined
}
};
/**
* Retrieves the values of tags marked as os
*
* @param tags an ExceptionItem's tags
*/
export const getOperatingSystems = (tags: string[]): string[] => {
return tags.filter((tag) => tag.startsWith('os:')).map((os) => os.substring(3).trim());
};
/**
* Formats os value array to a displayable string
*/
export const formatOperatingSystems = (osTypes: string[]): string => {
return osTypes
.filter((os) => ['linux', 'macos', 'windows'].includes(os))
.map((os) => {
if (os === 'macos') {
return 'macOS';
@ -121,21 +113,6 @@ export const formatOperatingSystems = (osTypes: string[]): string => {
.join(', ');
};
/**
* Returns all tags that match a given regex
*/
export const getTagsInclude = ({
tags,
regex,
}: {
tags: string[];
regex: RegExp;
}): [boolean, string | null] => {
const matches: string[] | null = tags.join(';').match(regex);
const match = matches != null ? matches[1] : null;
return [matches != null, match];
};
/**
* Formats ExceptionItem.comments into EuiCommentList format
*
@ -158,18 +135,15 @@ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[]
}));
export const getNewExceptionItem = ({
listType,
listId,
namespaceType,
ruleName,
}: {
listType: ExceptionListType;
listId: string;
namespaceType: NamespaceType;
ruleName: string;
}): CreateExceptionListItemBuilderSchema => {
return {
_tags: [listType],
comments: [],
description: `${ruleName} - exception list item`,
entries: [
@ -326,14 +300,12 @@ export const enrichExistingExceptionItemWithComments = (
*/
export const enrichExceptionItemsWithOS = (
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
osTypes: string[]
osTypes: OsTypeArray
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
const osTags = osTypes.map((os) => `os:${os}`);
return exceptionItems.map((item: ExceptionListItemSchema | CreateExceptionListItemSchema) => {
const newTags = item._tags ? union(item._tags, osTags) : [...osTags];
return {
...item,
_tags: newTags,
os_types: osTypes,
};
});
};
@ -419,7 +391,6 @@ export const getCodeSignatureValue = (
* Returns the default values from the alert data to autofill new endpoint exceptions
*/
export const getPrepopulatedItem = ({
listType,
listId,
ruleName,
codeSignature,
@ -428,7 +399,6 @@ export const getPrepopulatedItem = ({
eventCode,
listNamespace = 'agnostic',
}: {
listType: ExceptionListType;
listId: string;
listNamespace?: NamespaceType;
ruleName: string;
@ -438,7 +408,7 @@ export const getPrepopulatedItem = ({
eventCode: string;
}): ExceptionsBuilderExceptionItem => {
return {
...getNewExceptionItem({ listType, listId, namespaceType: listNamespace, ruleName }),
...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }),
entries: [
{
field: 'file.Ext.code_signature',
@ -459,7 +429,7 @@ export const getPrepopulatedItem = ({
],
},
{
field: 'file.path.text',
field: 'file.path.caseless',
operator: 'included',
type: 'match',
value: filePath ?? '',
@ -514,7 +484,6 @@ export const entryHasNonEcsType = (
* Returns the default values from the alert data to autofill new endpoint exceptions
*/
export const defaultEndpointExceptionItems = (
listType: ExceptionListType,
listId: string,
ruleName: string,
alertEcsData: Ecs
@ -523,7 +492,6 @@ export const defaultEndpointExceptionItems = (
return getCodeSignatureValue(alertEcsData).map((codeSignature) =>
getPrepopulatedItem({
listType,
listId,
ruleName,
filePath: file && file.path ? file.path[0] : '',

View file

@ -82,7 +82,6 @@ export const useFetchOrCreateRuleExceptionList = ({
type: exceptionListType,
namespace_type: 'single',
list_id: undefined,
_tags: undefined,
tags: undefined,
meta: undefined,
};

View file

@ -43,7 +43,6 @@ storiesOf('Components/ExceptionItem', module)
})
.add('with description', () => {
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.comments = [];
payload.entries = [
{
@ -66,7 +65,6 @@ storiesOf('Components/ExceptionItem', module)
})
.add('with comments', () => {
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = '';
payload.comments = getCommentsArrayMock();
payload.entries = [
@ -90,7 +88,6 @@ storiesOf('Components/ExceptionItem', module)
})
.add('with nested entries', () => {
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = '';
payload.comments = [];

View file

@ -175,10 +175,13 @@ describe('Exception viewer helpers', () => {
test('it returns formatted description list with a description if one specified', () => {
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = 'Im a description';
const result = getDescriptionListContent(payload);
const expected: DescriptionListItem[] = [
{
description: 'Linux',
title: 'OS',
},
{
description: 'April 20th 2020 @ 15:25:31',
title: 'Date created',
@ -198,10 +201,13 @@ describe('Exception viewer helpers', () => {
test('it returns just user and date created if no other fields specified', () => {
const payload = getExceptionListItemSchemaMock();
payload._tags = [];
payload.description = '';
const result = getDescriptionListContent(payload);
const expected: DescriptionListItem[] = [
{
description: 'Linux',
title: 'OS',
},
{
description: 'April 20th 2020 @ 15:25:31',
title: 'Date created',

View file

@ -6,12 +6,7 @@
import moment from 'moment';
import { entriesNested, ExceptionListItemSchema } from '../../../../lists_plugin_deps';
import {
getEntryValue,
getExceptionOperatorSelect,
formatOperatingSystems,
getOperatingSystems,
} from '../helpers';
import { getEntryValue, getExceptionOperatorSelect, formatOperatingSystems } from '../helpers';
import { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types';
import * as i18n from '../translations';
@ -80,7 +75,7 @@ export const getDescriptionListContent = (
const details = [
{
title: i18n.OPERATING_SYSTEM,
value: formatOperatingSystems(getOperatingSystems(exceptionItem._tags ?? [])),
value: formatOperatingSystems(exceptionItem.os_types),
},
{
title: i18n.DATE_CREATED,

View file

@ -83,7 +83,7 @@ export const ConditionEntry = memo<ConditionEntryProps>(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path',
{ defaultMessage: 'Path' }
),
value: 'process.executable.text',
value: 'process.executable.caseless',
},
];
}, []);

View file

@ -30,7 +30,7 @@ storiesOf('TrustedApps|TrustedAppCard', module)
trustedApp.created_at = '2020-09-17T14:52:33.899Z';
trustedApp.entries = [
{
field: 'process.executable.text',
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: '/some/path/on/file/system',
@ -44,7 +44,7 @@ storiesOf('TrustedApps|TrustedAppCard', module)
trustedApp.created_at = '2020-09-17T14:52:33.899Z';
trustedApp.entries = [
{
field: 'process.executable.text',
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: '/some/path/on/file/system',

View file

@ -67,7 +67,7 @@ const generateTrustedAppEntry: (options?: GenerateTrustedAppEntryOptions) => obj
return {
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
item_id: `generator_endpoint_trusted_apps_${generateUUID()}`,
_tags: ['endpoint', `os:${os}`],
os_types: [os],
tags: ['user added string for a tag', 'malware'],
type: 'simple',
description: 'This is a sample agnostic endpoint trusted app entry',

View file

@ -62,7 +62,7 @@ describe('buildEventTypeSignal', () => {
test('it should convert simple fields', async () => {
const testEntries: EntriesArray = [
{ field: 'server.domain', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' },
{ field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
{ field: 'host.hostname', operator: 'included', type: 'match', value: 'estc' },
];
@ -71,10 +71,10 @@ describe('buildEventTypeSignal', () => {
type: 'simple',
entries: [
{
field: 'server.domain',
field: 'host.os.full',
operator: 'included',
type: 'exact_cased',
value: 'DOMAIN',
value: 'windows',
},
{
field: 'server.ip',
@ -108,10 +108,10 @@ describe('buildEventTypeSignal', () => {
test('it should convert fields case sensitive', async () => {
const testEntries: EntriesArray = [
{ field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
{ field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
{
field: 'host.hostname.text',
field: 'host.hostname.caseless',
operator: 'included',
type: 'match_any',
value: ['estc', 'kibana'],
@ -122,10 +122,10 @@ describe('buildEventTypeSignal', () => {
type: 'simple',
entries: [
{
field: 'server.domain',
field: 'host.os.full',
operator: 'included',
type: 'exact_caseless',
value: 'DOMAIN',
value: 'windows',
},
{
field: 'server.ip',
@ -159,12 +159,12 @@ describe('buildEventTypeSignal', () => {
test('it should deduplicate exception entries', async () => {
const testEntries: EntriesArray = [
{ field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
{ field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
{ field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
{ field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
{
field: 'host.hostname.text',
field: 'host.hostname',
operator: 'included',
type: 'match_any',
value: ['estc', 'kibana'],
@ -175,10 +175,10 @@ describe('buildEventTypeSignal', () => {
type: 'simple',
entries: [
{
field: 'server.domain',
field: 'host.os.full',
operator: 'included',
type: 'exact_caseless',
value: 'DOMAIN',
value: 'windows',
},
{
field: 'server.ip',
@ -189,7 +189,7 @@ describe('buildEventTypeSignal', () => {
{
field: 'host.hostname',
operator: 'included',
type: 'exact_caseless_any',
type: 'exact_cased_any',
value: ['estc', 'kibana'],
},
],
@ -264,7 +264,7 @@ describe('buildEventTypeSignal', () => {
test('it should deduplicate exception items', async () => {
const testEntries: EntriesArray = [
{ field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
{ field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
];
@ -272,10 +272,10 @@ describe('buildEventTypeSignal', () => {
type: 'simple',
entries: [
{
field: 'server.domain',
field: 'host.os.full',
operator: 'included',
type: 'exact_caseless',
value: 'DOMAIN',
value: 'windows',
},
{
field: 'server.ip',
@ -308,9 +308,9 @@ describe('buildEventTypeSignal', () => {
test('it should ignore unsupported entries', async () => {
// Lists and exists are not supported by the Endpoint
const testEntries: EntriesArray = [
{ field: 'server.domain', operator: 'included', type: 'match', value: 'DOMAIN' },
{ field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' },
{
field: 'server.domain',
field: 'host.os.full',
operator: 'included',
type: 'list',
list: {
@ -325,10 +325,10 @@ describe('buildEventTypeSignal', () => {
type: 'simple',
entries: [
{
field: 'server.domain',
field: 'host.os.full',
operator: 'included',
type: 'exact_cased',
value: 'DOMAIN',
value: 'windows',
},
],
};

View file

@ -88,7 +88,7 @@ export async function getFullEndpointExceptionList(
const response = await eClient.findExceptionListItem({
listId,
namespaceType: 'agnostic',
filter: `exception-list-agnostic.attributes._tags:\"os:${os}\"`,
filter: `exception-list-agnostic.attributes.os_types:\"${os}\"`,
perPage: 100,
page,
sortField: 'created_at',
@ -141,16 +141,16 @@ export function translateToEndpointExceptions(
function getMatcherFunction(field: string, matchAny?: boolean): TranslatedEntryMatcher {
return matchAny
? field.endsWith('.text')
? field.endsWith('.caseless')
? 'exact_caseless_any'
: 'exact_cased_any'
: field.endsWith('.text')
: field.endsWith('.caseless')
? 'exact_caseless'
: 'exact_cased';
}
function normalizeFieldName(field: string): string {
return field.endsWith('.text') ? field.substring(0, field.length - 5) : field;
return field.endsWith('.caseless') ? field.substring(0, field.lastIndexOf('.')) : field;
}
function translateItem(

View file

@ -133,7 +133,6 @@ describe('when invoking endpoint trusted apps route handlers', () => {
const emptyResponse: FoundExceptionListItemSchema = {
data: [
{
_tags: ['os:windows'],
_version: undefined,
comments: [],
created_at: '2020-09-21T19:43:48.240Z',
@ -165,6 +164,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
meta: undefined,
name: 'test',
namespace_type: 'agnostic',
os_types: ['windows'],
tags: [],
tie_breaker_id: '1',
type: 'simple',
@ -240,7 +240,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
os: 'windows',
entries: [
{
field: 'process.executable.text',
field: 'process.executable.caseless',
type: 'match',
operator: 'included',
value: 'c:/programs files/Anti-Virus',
@ -267,6 +267,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
return ({
...getExceptionListItemSchemaMock(),
...newExceptionItem,
os_types: newExceptionItem.osTypes,
} as unknown) as ExceptionListItemSchema;
});
});
@ -288,12 +289,11 @@ describe('when invoking endpoint trusted apps route handlers', () => {
const request = createPostRequest();
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0]).toEqual({
_tags: ['os:windows'],
comments: [],
description: 'this one is ok',
entries: [
{
field: 'process.executable.text',
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
@ -304,6 +304,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
meta: undefined,
name: 'Some Anti-Virus App',
namespaceType: 'agnostic',
osTypes: ['windows'],
tags: [],
type: 'simple',
});
@ -320,7 +321,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
description: 'this one is ok',
entries: [
{
field: 'process.executable.text',
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
@ -357,7 +358,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
it('should trim condition entry values', async () => {
const newTrustedApp = createNewTrustedAppBody();
newTrustedApp.entries.push({
field: 'process.executable.text',
field: 'process.executable.caseless',
value: '\n some value \r\n ',
operator: 'included',
type: 'match',
@ -366,13 +367,13 @@ describe('when invoking endpoint trusted apps route handlers', () => {
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.executable.text',
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',
},
{
field: 'process.executable.text',
field: 'process.executable.caseless',
value: 'some value',
operator: 'included',
type: 'match',
@ -392,7 +393,7 @@ describe('when invoking endpoint trusted apps route handlers', () => {
await routeHandler(context, request, response);
expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([
{
field: 'process.executable.text',
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value: 'c:/programs files/Anti-Virus',

View file

@ -20,8 +20,8 @@ export const exceptionItemToTrustedAppItem = (
exceptionListItem: ExceptionListItemSchema
): TrustedApp => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { entries, description, created_by, created_at, name, _tags, id } = exceptionListItem;
const os = osFromTagsList(_tags);
const { entries, description, created_by, created_at, name, os_types, id } = exceptionListItem;
const os = os_types.length ? os_types[0] : 'unknown';
return {
entries: entries.map((entry) => {
if (entry.field.startsWith('process.hash')) {
@ -41,19 +41,6 @@ export const exceptionItemToTrustedAppItem = (
} as TrustedApp;
};
/**
* Retrieves the OS entry from a list of tags (property returned with ExcptionListItem).
* For Trusted Apps each entry must have at MOST 1 OS.
* */
const osFromTagsList = (tags: string[]): TrustedApp['os'] | 'unknown' => {
for (const tag of tags) {
if (tag.startsWith('os:')) {
return tag.substr(3) as TrustedApp['os'];
}
}
return 'unknown';
};
export const newTrustedAppItemToExceptionItem = ({
os,
entries,
@ -61,7 +48,6 @@ export const newTrustedAppItemToExceptionItem = ({
description = '',
}: NewTrustedApp): NewExceptionItem => {
return {
_tags: tagsListFromOs(os),
comments: [],
description,
// @ts-ignore
@ -83,15 +69,12 @@ export const newTrustedAppItemToExceptionItem = ({
meta: undefined,
name: name.trim(),
namespaceType: 'agnostic',
osTypes: [os],
tags: [],
type: 'simple',
};
};
const tagsListFromOs = (os: NewTrustedApp['os']): NewExceptionItem['_tags'] => {
return [`os:${os}`];
};
const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => {
switch (hash.length) {
case 32: