From f2d59ef7e5d8e236932409ab770cbf0508d4c2e5 Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Mon, 30 Nov 2020 15:42:31 +0100 Subject: [PATCH] Trusted Apps signer API. (#83661) * Separated out service layer for trusted apps. * Improved the type structure a bit to avoid using explicit string literals and to add possibility to return OS specific parts of trusted app object in type safe manner. * Added support for mapping of trusted app to exception item and back. * Changed schema to support signer in the API. * Renamed utils to mapping. * Exported some types in lists plugin and used them in trusted apps. * Added tests for mapping. * Added tests for service. * Switched deletion to use exceptions for not found case. * Added resetting of the mocks in service layer tests. * Added handlers tests. * Refactored mapping tests to be more granular based on the case. * Restored lowercasing of hash. * Added schema tests for signer field. * Removed the grouped tests (they were split into tests for separate concerns). * Corrected the tests. * Lowercased the hashes in the service test. * Moved the lowercasing to the right location. * Fixed the tests. * Added test for lowercasing hash value. * Introduced OperatingSystem enum instead of current types. * Removed os list constant in favour of separate lists in places that use it (each place has own needs to the ordering). * Fixed the missed OperatingSystem enum usage. --- x-pack/plugins/lists/common/shared_exports.ts | 1 + x-pack/plugins/lists/server/index.ts | 1 + .../common/endpoint/constants.ts | 1 - .../endpoint/schema/trusted_apps.test.ts | 364 +++++------- .../common/endpoint/schema/trusted_apps.ts | 77 ++- .../common/endpoint/types/os.ts | 9 +- .../common/endpoint/types/trusted_apps.ts | 44 +- .../public/management/common/translations.ts | 6 +- .../components/config_form/index.stories.tsx | 15 +- .../antivirus_registration/index.tsx | 3 +- .../policy/view/policy_forms/events/linux.tsx | 6 +- .../policy/view/policy_forms/events/mac.tsx | 6 +- .../view/policy_forms/events/windows.tsx | 12 +- .../view/policy_forms/protections/malware.tsx | 16 +- .../pages/trusted_apps/state/type_guards.ts | 29 +- .../pages/trusted_apps/test_utils/index.ts | 10 +- .../create_trusted_app_form.test.tsx | 17 +- .../components/create_trusted_app_form.tsx | 82 ++- .../components/condition_entry.tsx | 13 +- .../components/condition_group.tsx | 4 +- .../trusted_app_card/index.stories.tsx | 10 +- .../pages/trusted_apps/view/translations.ts | 17 +- .../routes/trusted_apps/handlers.test.ts | 233 ++++++++ .../endpoint/routes/trusted_apps/handlers.ts | 70 +-- .../routes/trusted_apps/mapping.test.ts | 425 ++++++++++++++ .../endpoint/routes/trusted_apps/mapping.ts | 185 +++++++ .../routes/trusted_apps/service.test.ts | 123 +++++ .../endpoint/routes/trusted_apps/service.ts | 79 +++ .../routes/trusted_apps/trusted_apps.test.ts | 522 ------------------ .../endpoint/routes/trusted_apps/utils.ts | 87 --- 30 files changed, 1430 insertions(+), 1037 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts delete mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts delete mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index ec9358c2cb503..9d9896e7ff898 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -23,6 +23,7 @@ export { EntryList, EntriesArray, NamespaceType, + NestedEntriesArray, Operator, OperatorEnum, OperatorTypeEnum, diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 31f22108028a6..ea27073e3053d 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -11,6 +11,7 @@ import { ListPlugin } from './plugin'; // exporting these since its required at top level in siem plugin export { ListClient } from './services/lists/list_client'; +export { CreateExceptionListItemOptions } from './services/exception_lists/exception_list_client_types'; export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export { ListPluginSetup } from './types'; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 8e19c2e8f219d..40e80840909a7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -14,7 +14,6 @@ export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency'; export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; -export const TRUSTED_APPS_SUPPORTED_OS_TYPES: readonly string[] = ['macos', 'windows', 'linux']; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index f83496737bcc6..fc99268f6f031 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -5,6 +5,7 @@ */ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps'; +import { ConditionEntryField, OperatingSystem } from '../types'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -70,93 +71,62 @@ describe('When invoking Trusted Apps Schema', () => { }); describe('for POST Create', () => { - const getCreateTrustedAppItem = () => ({ + const createConditionEntry = (data?: T) => ({ + field: ConditionEntryField.PATH, + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + ...(data || {}), + }); + const createNewTrustedApp = (data?: T) => ({ name: 'Some Anti-Virus App', description: 'this one is ok', os: 'windows', - entries: [ - { - field: 'process.executable.caseless', - type: 'match', - operator: 'included', - value: 'c:/programs files/Anti-Virus', - }, - ], + entries: [createConditionEntry()], + ...(data || {}), }); const body = PostTrustedAppCreateRequestSchema.body; it('should not error on a valid message', () => { - const bodyMsg = getCreateTrustedAppItem(); + const bodyMsg = createNewTrustedApp(); expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); }); it('should validate `name` is required', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - name: undefined, - }; - expect(() => body.validate(bodyMsg)).toThrow(); + expect(() => body.validate(createNewTrustedApp({ name: undefined }))).toThrow(); }); it('should validate `name` value to be non-empty', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - name: '', - }; - expect(() => body.validate(bodyMsg)).toThrow(); + expect(() => body.validate(createNewTrustedApp({ name: '' }))).toThrow(); }); it('should validate `description` as optional', () => { - const { description, ...bodyMsg } = getCreateTrustedAppItem(); + const { description, ...bodyMsg } = createNewTrustedApp(); expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); }); it('should validate `os` to to only accept known values', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - os: undefined, - }; + const bodyMsg = createNewTrustedApp({ os: undefined }); expect(() => body.validate(bodyMsg)).toThrow(); - const bodyMsg2 = { - ...bodyMsg, - os: '', - }; - expect(() => body.validate(bodyMsg2)).toThrow(); + expect(() => body.validate({ ...bodyMsg, os: '' })).toThrow(); - const bodyMsg3 = { - ...bodyMsg, - os: 'winz', - }; - expect(() => body.validate(bodyMsg3)).toThrow(); + expect(() => body.validate({ ...bodyMsg, os: 'winz' })).toThrow(); - ['linux', 'macos', 'windows'].forEach((os) => { - expect(() => { - body.validate({ - ...bodyMsg, - os, - }); - }).not.toThrow(); + [OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].forEach((os) => { + expect(() => body.validate({ ...bodyMsg, os })).not.toThrow(); }); }); it('should validate `entries` as required', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: undefined, - }; - expect(() => body.validate(bodyMsg)).toThrow(); + expect(() => body.validate(createNewTrustedApp({ entries: undefined }))).toThrow(); - const { entries, ...bodyMsg2 } = getCreateTrustedAppItem(); + const { entries, ...bodyMsg2 } = createNewTrustedApp(); expect(() => body.validate(bodyMsg2)).toThrow(); }); it('should validate `entries` to have at least 1 item', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [], - }; - expect(() => body.validate(bodyMsg)).toThrow(); + expect(() => body.validate(createNewTrustedApp({ entries: [] }))).toThrow(); }); describe('when `entries` are defined', () => { @@ -165,171 +135,163 @@ describe('When invoking Trusted Apps Schema', () => { const VALID_HASH_SHA1 = 'aedb279e378BED6C2DB3C9DC9e12ba635e0b391c'; const VALID_HASH_SHA256 = 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476'; - const getTrustedAppItemEntryItem = () => getCreateTrustedAppItem().entries[0]; - it('should validate `entry.field` is required', () => { - const { field, ...entry } = getTrustedAppItemEntryItem(); - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [entry], - }; + const { field, ...entry } = createConditionEntry(); + expect(() => body.validate(createNewTrustedApp({ entries: [entry] }))).toThrow(); + }); + + it('should validate `entry.field` does not accept empty values', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry({ field: '' })], + }); expect(() => body.validate(bodyMsg)).toThrow(); }); - it('should validate `entry.field` is limited to known values', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - field: '', - }, - ], - }; + it('should validate `entry.field` does not accept unknown values', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry({ field: 'invalid value' })], + }); expect(() => body.validate(bodyMsg)).toThrow(); + }); - const bodyMsg2 = { - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - field: 'invalid value', - }, - ], - }; - expect(() => body.validate(bodyMsg2)).toThrow(); - - [ - { - field: 'process.hash.*', - value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476', - }, - { field: 'process.executable.caseless', value: '/tmp/dir1' }, - ].forEach((partialEntry) => { - const bodyMsg3 = { - ...getCreateTrustedAppItem(), + it('should validate `entry.field` accepts hash field name for all os values', () => { + [OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].forEach((os) => { + const bodyMsg3 = createNewTrustedApp({ + os, entries: [ - { - ...getTrustedAppItemEntryItem(), - ...partialEntry, - }, + createConditionEntry({ + field: ConditionEntryField.HASH, + value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }), ], - }; + }); + + expect(() => body.validate(bodyMsg3)).not.toThrow(); + }); + }); + + it('should validate `entry.field` accepts path field name for all os values', () => { + [OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].forEach((os) => { + const bodyMsg3 = createNewTrustedApp({ + os, + entries: [ + createConditionEntry({ field: ConditionEntryField.PATH, value: '/tmp/dir1' }), + ], + }); expect(() => body.validate(bodyMsg3)).not.toThrow(); }); }); - it('should validate `entry.type` is limited to known values', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), + it('should validate `entry.field` accepts signer field name for windows os value', () => { + const bodyMsg3 = createNewTrustedApp({ + os: 'windows', entries: [ - { - ...getTrustedAppItemEntryItem(), - type: 'invalid', - }, + createConditionEntry({ field: ConditionEntryField.SIGNER, value: 'Microsoft' }), ], - }; + }); + + expect(() => body.validate(bodyMsg3)).not.toThrow(); + }); + + it('should validate `entry.field` does not accept signer field name for linux and macos os values', () => { + [OperatingSystem.LINUX, OperatingSystem.MAC].forEach((os) => { + const bodyMsg3 = createNewTrustedApp({ + os, + entries: [ + createConditionEntry({ field: ConditionEntryField.SIGNER, value: 'Microsoft' }), + ], + }); + + expect(() => body.validate(bodyMsg3)).toThrow(); + }); + }); + + it('should validate `entry.type` does not accept unknown values', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry({ type: 'invalid' })], + }); expect(() => body.validate(bodyMsg)).toThrow(); + }); - // Allow `match` - const bodyMsg2 = { - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - type: 'match', - }, - ], - }; - expect(() => body.validate(bodyMsg2)).not.toThrow(); + it('should validate `entry.type` accepts known values', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry({ type: 'match' })], + }); + expect(() => body.validate(bodyMsg)).not.toThrow(); }); - it('should validate `entry.operator` is limited to known values', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - operator: 'invalid', - }, - ], - }; + it('should validate `entry.operator` does not accept unknown values', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry({ operator: 'invalid' })], + }); expect(() => body.validate(bodyMsg)).toThrow(); + }); - // Allow `match` - const bodyMsg2 = { - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - operator: 'included', - }, - ], - }; - expect(() => body.validate(bodyMsg2)).not.toThrow(); + it('should validate `entry.operator` accepts known values', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry({ operator: 'included' })], + }); + expect(() => body.validate(bodyMsg)).not.toThrow(); }); it('should validate `entry.value` required', () => { - const { value, ...entry } = getTrustedAppItemEntryItem(); - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [entry], - }; - expect(() => body.validate(bodyMsg)).toThrow(); + const { value, ...entry } = createConditionEntry(); + expect(() => body.validate(createNewTrustedApp({ entries: [entry] }))).toThrow(); }); it('should validate `entry.value` is non-empty', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - value: '', - }, - ], - }; + const bodyMsg = createNewTrustedApp({ entries: [createConditionEntry({ value: '' })] }); expect(() => body.validate(bodyMsg)).toThrow(); }); - it('should validate that `entry.field` is used only once', () => { - let bodyMsg = { - ...getCreateTrustedAppItem(), - entries: [getTrustedAppItemEntryItem(), getTrustedAppItemEntryItem()], - }; + it('should validate that `entry.field` path field value can only be used once', () => { + const bodyMsg = createNewTrustedApp({ + entries: [createConditionEntry(), createConditionEntry()], + }); expect(() => body.validate(bodyMsg)).toThrow('[Path] field can only be used once'); + }); - bodyMsg = { - ...getCreateTrustedAppItem(), + it('should validate that `entry.field` hash field value can only be used once', () => { + const bodyMsg = createNewTrustedApp({ entries: [ - { - ...getTrustedAppItemEntryItem(), - field: 'process.hash.*', + createConditionEntry({ + field: ConditionEntryField.HASH, value: VALID_HASH_MD5, - }, - { - ...getTrustedAppItemEntryItem(), - field: 'process.hash.*', + }), + createConditionEntry({ + field: ConditionEntryField.HASH, value: VALID_HASH_MD5, - }, + }), ], - }; + }); expect(() => body.validate(bodyMsg)).toThrow('[Hash] field can only be used once'); }); + it('should validate that `entry.field` signer field value can only be used once', () => { + const bodyMsg = createNewTrustedApp({ + entries: [ + createConditionEntry({ + field: ConditionEntryField.SIGNER, + value: 'Microsoft', + }), + createConditionEntry({ + field: ConditionEntryField.SIGNER, + value: 'Microsoft', + }), + ], + }); + expect(() => body.validate(bodyMsg)).toThrow('[Signer] field can only be used once'); + }); + it('should validate Hash field valid value', () => { [VALID_HASH_MD5, VALID_HASH_SHA1, VALID_HASH_SHA256].forEach((value) => { expect(() => { - body.validate({ - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - field: 'process.hash.*', - value, - }, - ], - }); + body.validate( + createNewTrustedApp({ + entries: [createConditionEntry({ field: ConditionEntryField.HASH, value })], + }) + ); }).not.toThrow(); }); }); @@ -337,49 +299,29 @@ describe('When invoking Trusted Apps Schema', () => { it('should validate Hash value with invalid length', () => { ['xyz', VALID_HASH_SHA256 + VALID_HASH_MD5].forEach((value) => { expect(() => { - body.validate({ - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - field: 'process.hash.*', - value, - }, - ], - }); + body.validate( + createNewTrustedApp({ + entries: [createConditionEntry({ field: ConditionEntryField.HASH, value })], + }) + ); }).toThrow(); }); }); it('should validate Hash value with invalid characters', () => { expect(() => { - body.validate({ - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - field: 'process.hash.*', - value: `G${VALID_HASH_MD5.substr(1)}`, - }, - ], - }); + body.validate( + createNewTrustedApp({ + entries: [ + createConditionEntry({ + field: ConditionEntryField.HASH, + value: `G${VALID_HASH_MD5.substr(1)}`, + }), + ], + }) + ); }).toThrow(); }); - - it('should trim hash value before validation', () => { - expect(() => { - body.validate({ - ...getCreateTrustedAppItem(), - entries: [ - { - ...getTrustedAppItemEntryItem(), - field: 'process.hash.*', - value: ` ${VALID_HASH_MD5} \r\n`, - }, - ], - }); - }).not.toThrow(); - }); }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 74135169635bd..fee35e8f487a5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -4,22 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; -import { TrustedApp } from '../types'; +import { schema, Type } from '@kbn/config-schema'; +import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types'; -const hashLengths: readonly number[] = [ +const HASH_LENGTHS: readonly number[] = [ 32, // MD5 40, // SHA1 64, // SHA256 ]; -const hasInvalidCharacters = /[^0-9a-f]/i; +const INVALID_CHARACTERS_PATTERN = /[^0-9a-f]/i; -const entryFieldLabels: { [k in TrustedApp['entries'][0]['field']]: string } = { - 'process.hash.*': 'Hash', - 'process.executable.caseless': 'Path', - 'process.code_signature': 'Signer', +const entryFieldLabels: { [k in ConditionEntryField]: string } = { + [ConditionEntryField.HASH]: 'Hash', + [ConditionEntryField.PATH]: 'Path', + [ConditionEntryField.SIGNER]: 'Signer', }; +const isValidHash = (value: string) => + HASH_LENGTHS.includes(value.length) && !INVALID_CHARACTERS_PATTERN.test(value); + export const DeleteTrustedAppsRequestSchema = { params: schema.object({ id: schema.string(), @@ -33,17 +36,17 @@ export const GetTrustedAppsRequestSchema = { }), }; -export const PostTrustedAppCreateRequestSchema = { - body: schema.object({ +const createNewTrustedAppForOsScheme = ( + osSchema: Type, + fieldSchema: Type +) => + schema.object({ name: schema.string({ minLength: 1, maxLength: 256 }), description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })), - os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), + os: osSchema, entries: schema.arrayOf( schema.object({ - field: schema.oneOf([ - schema.literal('process.hash.*'), - schema.literal('process.executable.caseless'), - ]), + field: fieldSchema, type: schema.literal('match'), operator: schema.literal('included'), value: schema.string({ minLength: 1 }), @@ -51,27 +54,43 @@ export const PostTrustedAppCreateRequestSchema = { { minSize: 1, validate(entries) { - const usedFields: string[] = []; - for (const { field, value } of entries) { - if (usedFields.includes(field)) { + const usedFields = new Set(); + + for (const entry of entries) { + // unfortunately combination of generics and Type<...> for "field" causes type errors + const { field, value } = entry as ConditionEntry; + + if (usedFields.has(field)) { return `[${entryFieldLabels[field]}] field can only be used once`; } - usedFields.push(field); - - if (field === 'process.hash.*') { - const trimmedValue = value.trim(); + usedFields.add(field); - if ( - !hashLengths.includes(trimmedValue.length) || - hasInvalidCharacters.test(trimmedValue) - ) { - return `Invalid hash value [${value}]`; - } + if (field === ConditionEntryField.HASH && !isValidHash(value)) { + return `Invalid hash value [${value}]`; } } }, } ), - }), + }); + +export const PostTrustedAppCreateRequestSchema = { + body: schema.oneOf([ + createNewTrustedAppForOsScheme( + schema.oneOf([schema.literal(OperatingSystem.LINUX), schema.literal(OperatingSystem.MAC)]), + schema.oneOf([ + schema.literal(ConditionEntryField.HASH), + schema.literal(ConditionEntryField.PATH), + ]) + ), + createNewTrustedAppForOsScheme( + schema.literal(OperatingSystem.WINDOWS), + schema.oneOf([ + schema.literal(ConditionEntryField.HASH), + schema.literal(ConditionEntryField.PATH), + schema.literal(ConditionEntryField.SIGNER), + ]) + ), + ]), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/os.ts b/x-pack/plugins/security_solution/common/endpoint/types/os.ts index b9afbd63ecd54..df6ff4c3cd8dc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/os.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/os.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export type Linux = 'linux'; -export type MacOS = 'macos'; -export type Windows = 'windows'; -export type OperatingSystem = Linux | MacOS | Windows; +export enum OperatingSystem { + LINUX = 'linux', + MAC = 'macos', + WINDOWS = 'windows', +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 79d66443bc8f1..57e7ffff08051 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -11,7 +11,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../schema/trusted_apps'; -import { Linux, MacOS, Windows } from './os'; +import { OperatingSystem } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -33,33 +33,41 @@ export interface PostTrustedAppCreateResponse { data: TrustedApp; } -export interface MacosLinuxConditionEntry { - field: 'process.hash.*' | 'process.executable.caseless'; +export enum ConditionEntryField { + HASH = 'process.hash.*', + PATH = 'process.executable.caseless', + SIGNER = 'process.Ext.code_signature', +} + +export interface ConditionEntry { + field: T; type: 'match'; operator: 'included'; value: string; } -export type WindowsConditionEntry = - | MacosLinuxConditionEntry - | (Omit & { - field: 'process.code_signature'; - }); +export type MacosLinuxConditionEntry = ConditionEntry< + ConditionEntryField.HASH | ConditionEntryField.PATH +>; +export type WindowsConditionEntry = ConditionEntry< + ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER +>; + +export interface MacosLinuxConditionEntries { + os: OperatingSystem.LINUX | OperatingSystem.MAC; + entries: MacosLinuxConditionEntry[]; +} + +export interface WindowsConditionEntries { + os: OperatingSystem.WINDOWS; + entries: WindowsConditionEntry[]; +} /** Type for a new Trusted App Entry */ export type NewTrustedApp = { name: string; description?: string; -} & ( - | { - os: Linux | MacOS; - entries: MacosLinuxConditionEntry[]; - } - | { - os: Windows; - entries: WindowsConditionEntry[]; - } -); +} & (MacosLinuxConditionEntries | WindowsConditionEntries); /** A trusted app entry */ export type TrustedApp = NewTrustedApp & { diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index 415658c1fd6af..a2454bb0ec282 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -25,13 +25,13 @@ export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administr }); export const OS_TITLES: Readonly<{ [K in OperatingSystem]: string }> = { - windows: i18n.translate('xpack.securitySolution.administration.os.windows', { + [OperatingSystem.WINDOWS]: i18n.translate('xpack.securitySolution.administration.os.windows', { defaultMessage: 'Windows', }), - macos: i18n.translate('xpack.securitySolution.administration.os.macos', { + [OperatingSystem.MAC]: i18n.translate('xpack.securitySolution.administration.os.macos', { defaultMessage: 'Mac', }), - linux: i18n.translate('xpack.securitySolution.administration.os.linux', { + [OperatingSystem.LINUX]: i18n.translate('xpack.securitySolution.administration.os.linux', { defaultMessage: 'Linux', }), }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx index 4f288af393b7c..dcd2bdac281e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { storiesOf, addDecorator } from '@storybook/react'; +import { addDecorator, storiesOf } from '@storybook/react'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; + import { ConfigForm } from '.'; addDecorator((storyFn) => ( @@ -18,21 +20,24 @@ addDecorator((storyFn) => ( storiesOf('PolicyDetails/ConfigForm', module) .add('One OS', () => { return ( - + {'Some content'} ); }) .add('Multiple OSs', () => { return ( - + {'Some content'} ); }) .add('Complex content', () => { return ( - + {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' + 'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' + @@ -53,7 +58,7 @@ storiesOf('PolicyDetails/ConfigForm', module) const toggle = {}} />; return ( - + {'Some content'} ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx index 8d1ac29c8ce1e..21fe14df81dd2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx @@ -9,6 +9,7 @@ import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../../components/config_form'; @@ -36,7 +37,7 @@ export const AntivirusRegistrationForm = memo(() => { defaultMessage: 'Register as anti-virus', } )} - supportedOss={['windows']} + supportedOss={[OperatingSystem.WINDOWS]} > {i18n.translate( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index b43f93f1a1e2b..999e3bac5653a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -6,14 +6,14 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiText } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; import { COLLECTIONS_ENABLED_MESSAGE, EVENTS_FORM_TYPE_LABEL, @@ -85,7 +85,7 @@ export const LinuxEvents = React.memo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index fbbe50fbec1b0..6e15a3c4cd43b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -6,14 +6,14 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiText } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; import { COLLECTIONS_ENABLED_MESSAGE, EVENTS_FORM_TYPE_LABEL, @@ -85,7 +85,7 @@ export const MacEvents = React.memo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index cd2ef62c1cb84..c381249cf24b9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -6,14 +6,18 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiText } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { setIn, getIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; +import { getIn, setIn } from '../../../models/policy_details_config'; +import { + Immutable, + OperatingSystem, + UIPolicyConfig, +} from '../../../../../../../common/endpoint/types'; import { COLLECTIONS_ENABLED_MESSAGE, EVENTS_FORM_TYPE_LABEL, @@ -127,7 +131,7 @@ export const WindowsEvents = React.memo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index c5724956bc21f..c78455aa8d990 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -10,21 +10,25 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiCallOut, + EuiCheckbox, EuiRadio, + EuiSpacer, EuiSwitch, EuiText, - EuiSpacer, EuiTextArea, htmlIdGenerator, - EuiCallOut, - EuiCheckbox, } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; -import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; -import { OS, MalwareProtectionOSes } from '../../../types'; +import { + Immutable, + OperatingSystem, + ProtectionModes, +} from '../../../../../../../common/endpoint/types'; +import { MalwareProtectionOSes, OS } from '../../../types'; import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; @@ -305,7 +309,7 @@ export const MalwareProtections = React.memo(() => { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', { defaultMessage: 'Malware', })} - supportedOss={['windows', 'macos']} + supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC]} dataTestSubj="malwareProtectionsForm" rightCorner={protectionSwitch} > diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 23b13695b0595..fe2ab98edb588 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -11,12 +11,12 @@ import { TrustedAppCreateSuccess, } from './trusted_apps_list_page_state'; import { + ConditionEntry, + ConditionEntryField, Immutable, MacosLinuxConditionEntry, - NewTrustedApp, WindowsConditionEntry, } from '../../../../../common/endpoint/types'; -import { TRUSTED_APPS_SUPPORTED_OS_TYPES } from '../../../../../common/endpoint/constants'; type CreateViewPossibleStates = | TrustedAppsListPageState['createView'] @@ -40,23 +40,14 @@ export const isTrustedAppCreateFailureState = ( return data?.type === 'failure'; }; -export const isWindowsTrustedApp = ( - trustedApp: T -): trustedApp is T & { os: 'windows' } => { - return trustedApp.os === 'windows'; +export const isWindowsTrustedAppCondition = ( + condition: ConditionEntry +): condition is WindowsConditionEntry => { + return condition.field === ConditionEntryField.SIGNER || true; }; -export const isWindowsTrustedAppCondition = (condition: { - field: string; -}): condition is WindowsConditionEntry => { - return condition.field === 'process.code_signature' || true; +export const isMacosLinuxTrustedAppCondition = ( + condition: ConditionEntry +): condition is MacosLinuxConditionEntry => { + return condition.field !== ConditionEntryField.SIGNER; }; - -export const isMacosLinuxTrustedAppCondition = (condition: { - field: string; -}): condition is MacosLinuxConditionEntry => { - return condition.field !== 'process.code_signature' || true; -}; - -export const isTrustedAppSupportedOs = (os: string): os is NewTrustedApp['os'] => - TRUSTED_APPS_SUPPORTED_OS_TYPES.includes(os); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index d881b5cbcb5b0..66674c3665ce3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -5,7 +5,7 @@ */ import { combineReducers, createStore } from 'redux'; -import { TrustedApp } from '../../../../../common/endpoint/types'; +import { TrustedApp, OperatingSystem } from '../../../../../common/endpoint/types'; import { RoutingAction } from '../../../../common/store/routing'; import { @@ -31,7 +31,11 @@ import { import { trustedAppsPageReducer } from '../store/reducer'; import { TrustedAppsListResourceStateChanged } from '../store/action'; -const OS_LIST: Array = ['windows', 'macos', 'linux']; +const OPERATING_SYSTEMS: OperatingSystem[] = [ + OperatingSystem.WINDOWS, + OperatingSystem.MAC, + OperatingSystem.LINUX, +]; const generate = (count: number, generator: (i: number) => T) => [...new Array(count).keys()].map(generator); @@ -43,7 +47,7 @@ export const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedA description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '), created_at: '1 minute ago', created_by: 'someone', - os: OS_LIST[i % 3], + os: OPERATING_SYSTEMS[i % 3], entries: [], }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 4bac9164e1d62..56c1b3af77aa5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import * as reactTestingLibrary from '@testing-library/react'; +import { fireEvent, getByTestId } from '@testing-library/dom'; + +import { ConditionEntryField, OperatingSystem } from '../../../../../../common/endpoint/types'; import { AppContextTestRender, createAppRootMockRenderer, } from '../../../../../common/mock/endpoint'; -import * as reactTestingLibrary from '@testing-library/react'; -import React from 'react'; + import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form'; -import { fireEvent, getByTestId } from '@testing-library/dom'; describe('When showing the Trusted App Create Form', () => { const dataTestSubjForForm = 'createForm'; @@ -234,14 +237,14 @@ describe('When showing the Trusted App Create Form', () => { description: '', entries: [ { - field: 'process.hash.*', + field: ConditionEntryField.HASH, operator: 'included', type: 'match', value: '', }, ], name: '', - os: 'windows', + os: OperatingSystem.WINDOWS, }, }); }); @@ -289,14 +292,14 @@ describe('When showing the Trusted App Create Form', () => { description: 'some description', entries: [ { - field: 'process.hash.*', + field: ConditionEntryField.HASH, operator: 'included', type: 'match', value: 'someHASH', }, ], name: 'Some Process', - os: 'windows', + os: OperatingSystem.WINDOWS, }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index b8692df0240fa..0a449bba7ca43 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -6,34 +6,39 @@ import React, { ChangeEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { + EuiFieldText, EuiForm, EuiFormRow, - EuiFieldText, EuiSuperSelect, EuiSuperSelectOption, EuiTextArea, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiFormProps } from '@elastic/eui/src/components/form/form'; -import { TRUSTED_APPS_SUPPORTED_OS_TYPES } from '../../../../../../common/endpoint/constants'; import { LogicalConditionBuilder } from './logical_condition'; import { + ConditionEntry, + ConditionEntryField, MacosLinuxConditionEntry, NewTrustedApp, - TrustedApp, + OperatingSystem, } from '../../../../../../common/endpoint/types'; import { LogicalConditionBuilderProps } from './logical_condition/logical_condition_builder'; import { OS_TITLES } from '../translations'; import { isMacosLinuxTrustedAppCondition, - isTrustedAppSupportedOs, - isWindowsTrustedApp, isWindowsTrustedAppCondition, } from '../../state/type_guards'; -const generateNewEntry = (): NewTrustedApp['entries'][0] => { +const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ + OperatingSystem.MAC, + OperatingSystem.WINDOWS, + OperatingSystem.LINUX, +]; + +const generateNewEntry = (): ConditionEntry => { return { - field: 'process.hash.*', + field: ConditionEntryField.HASH, operator: 'included', type: 'match', value: '', @@ -160,18 +165,14 @@ export const CreateTrustedAppForm = memo( ({ fullWidth, onChange, ...formProps }) => { const dataTestSubj = formProps['data-test-subj']; - const osOptions: Array> = useMemo(() => { - return TRUSTED_APPS_SUPPORTED_OS_TYPES.map((os) => { - return { - value: os, - inputDisplay: OS_TITLES[os as TrustedApp['os']], - }; - }); - }, []); + const osOptions: Array> = useMemo( + () => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })), + [] + ); const [formValues, setFormValues] = useState({ name: '', - os: 'windows', + os: OperatingSystem.WINDOWS, entries: [generateNewEntry()], description: '', }); @@ -200,20 +201,20 @@ export const CreateTrustedAppForm = memo( const handleAndClick = useCallback(() => { setFormValues( (prevState): NewTrustedApp => { - if (isWindowsTrustedApp(prevState)) { + if (prevState.os === OperatingSystem.WINDOWS) { return { ...prevState, - entries: [...prevState.entries, generateNewEntry()].filter((entry) => - isWindowsTrustedAppCondition(entry) + entries: [...prevState.entries, generateNewEntry()].filter( + isWindowsTrustedAppCondition ), }; } else { return { ...prevState, entries: [ - ...prevState.entries.filter((entry) => isMacosLinuxTrustedAppCondition(entry)), + ...prevState.entries.filter(isMacosLinuxTrustedAppCondition), generateNewEntry(), - ] as MacosLinuxConditionEntry[], + ], }; } } @@ -245,30 +246,27 @@ export const CreateTrustedAppForm = memo( [] ); - const handleOsChange = useCallback<(v: string) => void>((newOsValue) => { + const handleOsChange = useCallback<(v: OperatingSystem) => void>((newOsValue) => { setFormValues( (prevState): NewTrustedApp => { - if (isTrustedAppSupportedOs(newOsValue)) { - const updatedState: NewTrustedApp = { - ...prevState, - entries: [], - os: newOsValue, - }; - if (!isWindowsTrustedApp(updatedState)) { - updatedState.entries.push( - ...(prevState.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry) - ) as MacosLinuxConditionEntry[]) - ); - if (updatedState.entries.length === 0) { - updatedState.entries.push(generateNewEntry() as MacosLinuxConditionEntry); - } - } else { - updatedState.entries.push(...prevState.entries); + const updatedState: NewTrustedApp = { + ...prevState, + entries: [], + os: newOsValue, + }; + if (updatedState.os !== OperatingSystem.WINDOWS) { + updatedState.entries.push( + ...(prevState.entries.filter((entry) => + isMacosLinuxTrustedAppCondition(entry) + ) as MacosLinuxConditionEntry[]) + ); + if (updatedState.entries.length === 0) { + updatedState.entries.push(generateNewEntry()); } - return updatedState; + } else { + updatedState.entries.push(...prevState.entries); } - return prevState; + return updatedState; } ); setWasVisited((prevState) => { @@ -294,7 +292,7 @@ export const CreateTrustedAppForm = memo( (newEntry, oldEntry) => { setFormValues( (prevState): NewTrustedApp => { - if (isWindowsTrustedApp(prevState)) { + if (prevState.os === OperatingSystem.WINDOWS) { return { ...prevState, entries: prevState.entries.map((item) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx index 020ab3f1cf1fd..df85244cd2813 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx @@ -5,6 +5,7 @@ */ import React, { ChangeEventHandler, memo, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, @@ -14,8 +15,8 @@ import { EuiButtonIcon, EuiSuperSelectOption, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { TrustedApp } from '../../../../../../../../common/endpoint/types'; + +import { ConditionEntryField, TrustedApp } from '../../../../../../../../common/endpoint/types'; import { CONDITION_FIELD_TITLE } from '../../../translations'; const ConditionEntryCell = memo<{ @@ -73,12 +74,12 @@ export const ConditionEntry = memo( const fieldOptions = useMemo>>(() => { return [ { - inputDisplay: CONDITION_FIELD_TITLE['process.hash.*'], - value: 'process.hash.*', + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.HASH], + value: ConditionEntryField.HASH, }, { - inputDisplay: CONDITION_FIELD_TITLE['process.executable.caseless'], - value: 'process.executable.caseless', + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.PATH], + value: ConditionEntryField.PATH, }, ]; }, []); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx index 5c8a55653a84f..9b961d87b7eb1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { NewTrustedApp, TrustedApp } from '../../../../../../../../common/endpoint/types'; +import { TrustedApp, WindowsConditionEntry } from '../../../../../../../../common/endpoint/types'; import { ConditionEntry, ConditionEntryProps } from './condition_entry'; import { AndOrBadge } from '../../../../../../../common/components/and_or_badge'; @@ -85,7 +85,7 @@ export const ConditionGroup = memo( )}
- {(entries as (NewTrustedApp & { os: 'windows' })['entries']).map((entry, index) => ( + {(entries as WindowsConditionEntry[]).map((entry, index) => ( ( )); const PATH_CONDITION: WindowsConditionEntry = { - field: 'process.executable.caseless', + field: ConditionEntryField.PATH, operator: 'included', type: 'match', value: '/some/path/on/file/system', }; const SIGNER_CONDITION: WindowsConditionEntry = { - field: 'process.code_signature', + field: ConditionEntryField.SIGNER, operator: 'included', type: 'match', value: 'Elastic', diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index 4c2b3f0e59ccb..4e31890da84d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -9,33 +9,34 @@ import { TrustedApp, MacosLinuxConditionEntry, WindowsConditionEntry, + ConditionEntry, + ConditionEntryField, } from '../../../../../common/endpoint/types'; export { OS_TITLES } from '../../../common/translations'; export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: - 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', + 'Add a trusted application to improve performance or alleviate conflicts with other applications ' + + 'running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', }); -type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; - -export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { - 'process.hash.*': i18n.translate( +export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { + [ConditionEntryField.HASH]: i18n.translate( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash', { defaultMessage: 'Hash' } ), - 'process.executable.caseless': i18n.translate( + [ConditionEntryField.PATH]: i18n.translate( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path', { defaultMessage: 'Path' } ), - 'process.code_signature': i18n.translate( + [ConditionEntryField.SIGNER]: i18n.translate( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature', { defaultMessage: 'Signature' } ), }; -export const OPERATOR_TITLE: { [K in Entry['operator']]: string } = { +export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = { included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { defaultMessage: 'is', }), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts new file mode 100644 index 0000000000000..e87a8ada6bf2b --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -0,0 +1,233 @@ +/* + * 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 { KibanaResponseFactory } from 'kibana/server'; + +import { xpackMocks } from '../../../../../../mocks'; +import { loggingSystemMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; +import { listMock } from '../../../../../lists/server/mocks'; +import { ExceptionListClient } from '../../../../../lists/server'; +import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; + +import { ConditionEntryField, OperatingSystem } from '../../../../common/endpoint/types'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; +import { createConditionEntry, createEntryMatch } from './mapping'; +import { + getTrustedAppsCreateRouteHandler, + getTrustedAppsDeleteRouteHandler, + getTrustedAppsListRouteHandler, +} from './handlers'; + +const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; + +const createAppContextMock = () => ({ + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), +}); + +const createHandlerContextMock = () => ({ + ...xpackMocks.createRequestHandlerContext(), + lists: { + getListClient: jest.fn(), + getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient), + }, +}); + +const assertResponse = ( + response: jest.Mocked, + expectedResponseType: keyof KibanaResponseFactory, + expectedResponseBody: T +) => { + expect(response[expectedResponseType]).toBeCalled(); + expect(response[expectedResponseType].mock.calls[0][0]?.body).toEqual(expectedResponseBody); +}; + +const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = { + _version: '123', + id: '123', + comments: [], + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + description: 'Linux trusted app 1', + entries: [ + createEntryMatch('process.executable.caseless', '/bin/malware'), + createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241'), + ], + item_id: '123', + list_id: 'endpoint_trusted_apps', + meta: undefined, + name: 'linux trusted app 1', + namespace_type: 'agnostic', + os_types: ['linux'], + tags: [], + type: 'simple', + tie_breaker_id: '123', + updated_at: '11/11/2011T11:11:11.111', + updated_by: 'admin', +}; + +const NEW_TRUSTED_APP = { + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), + createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + ], +}; + +const TRUSTED_APP = { + id: '123', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), + ], +}; + +describe('handlers', () => { + const appContextMock = createAppContextMock(); + + beforeEach(() => { + exceptionsListClient.deleteExceptionListItem.mockReset(); + exceptionsListClient.createExceptionListItem.mockReset(); + exceptionsListClient.findExceptionListItem.mockReset(); + exceptionsListClient.createTrustedAppsList.mockReset(); + + appContextMock.logFactory.get.mockClear(); + (appContextMock.logFactory.get().error as jest.Mock).mockClear(); + }); + + describe('getTrustedAppsDeleteRouteHandler', () => { + const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(appContextMock); + + it('should return ok when trusted app deleted', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + + exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); + + await deleteTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ params: { id: '123' } }), + mockResponse + ); + + assertResponse(mockResponse, 'ok', undefined); + }); + + it('should return notFound when trusted app missing', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + + await deleteTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ params: { id: '123' } }), + mockResponse + ); + + assertResponse(mockResponse, 'notFound', 'trusted app id [123] not found'); + }); + + it('should return internalError when errors happen', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + const error = new Error('Something went wrong'); + + exceptionsListClient.deleteExceptionListItem.mockRejectedValue(error); + + await deleteTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ params: { id: '123' } }), + mockResponse + ); + + assertResponse(mockResponse, 'internalError', error); + expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + }); + }); + + describe('getTrustedAppsCreateRouteHandler', () => { + const createTrustedAppHandler = getTrustedAppsCreateRouteHandler(appContextMock); + + it('should return ok with body when trusted app created', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + + exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); + + await createTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ); + + assertResponse(mockResponse, 'ok', { data: TRUSTED_APP }); + }); + + it('should return internalError when errors happen', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + const error = new Error('Something went wrong'); + + exceptionsListClient.createExceptionListItem.mockRejectedValue(error); + + await createTrustedAppHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ); + + assertResponse(mockResponse, 'internalError', error); + expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + }); + }); + + describe('getTrustedAppsListRouteHandler', () => { + const getTrustedAppsListHandler = getTrustedAppsListRouteHandler(appContextMock); + + it('should return ok with list when no errors', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + + exceptionsListClient.findExceptionListItem.mockResolvedValue({ + data: [EXCEPTION_LIST_ITEM], + page: 1, + per_page: 20, + total: 100, + }); + + await getTrustedAppsListHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ params: { page: 1, per_page: 20 } }), + mockResponse + ); + + assertResponse(mockResponse, 'ok', { + data: [TRUSTED_APP], + page: 1, + per_page: 20, + total: 100, + }); + }); + + it('should return internalError when errors happen', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + const error = new Error('Something went wrong'); + + exceptionsListClient.findExceptionListItem.mockRejectedValue(error); + + await getTrustedAppsListHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), + mockResponse + ); + + assertResponse(mockResponse, 'internalError', error); + expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index 7cf3d467c10c2..36e253d9eb3b6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -5,16 +5,22 @@ */ import { RequestHandler, RequestHandlerContext } from 'kibana/server'; + +import { ExceptionListClient } from '../../../../../lists/server'; + import { DeleteTrustedAppsRequestParams, GetTrustedAppsListRequest, - GetTrustedListAppsResponse, PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; + import { EndpointAppContext } from '../../types'; -import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { ExceptionListClient } from '../../../../../lists/server'; +import { + createTrustedApp, + deleteTrustedApp, + getTrustedAppsList, + MissingTrustedAppException, +} from './service'; const exceptionListClientFromContext = (context: RequestHandlerContext): ExceptionListClient => { const exceptionLists = context.lists?.getExceptionListClient(); @@ -33,22 +39,16 @@ export const getTrustedAppsDeleteRouteHandler = ( return async (context, req, res) => { try { - const exceptionsListService = exceptionListClientFromContext(context); - const { id } = req.params; - const response = await exceptionsListService.deleteExceptionListItem({ - id, - itemId: undefined, - namespaceType: 'agnostic', - }); - - if (response === null) { - return res.notFound({ body: `trusted app id [${id}] not found` }); - } + await deleteTrustedApp(exceptionListClientFromContext(context), req.params); return res.ok(); } catch (error) { - logger.error(error); - return res.internalError({ body: error }); + if (error instanceof MissingTrustedAppException) { + return res.notFound({ body: `trusted app id [${req.params.id}] not found` }); + } else { + logger.error(error); + return res.internalError({ body: error }); + } } }; }; @@ -59,28 +59,10 @@ export const getTrustedAppsListRouteHandler = ( const logger = endpointAppContext.logFactory.get('trusted_apps'); return async (context, req, res) => { - const { page, per_page: perPage } = req.query; - try { - const exceptionsListService = exceptionListClientFromContext(context); - // Ensure list is created if it does not exist - await exceptionsListService.createTrustedAppsList(); - const results = await exceptionsListService.findExceptionListItem({ - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - page, - perPage, - filter: undefined, - namespaceType: 'agnostic', - sortField: 'name', - sortOrder: 'asc', + return res.ok({ + body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query), }); - const body: GetTrustedListAppsResponse = { - data: results?.data.map(exceptionItemToTrustedAppItem) ?? [], - total: results?.total ?? 0, - page: results?.page ?? 1, - per_page: results?.per_page ?? perPage!, - }; - return res.ok({ body }); } catch (error) { logger.error(error); return res.internalError({ body: error }); @@ -94,21 +76,9 @@ export const getTrustedAppsCreateRouteHandler = ( const logger = endpointAppContext.logFactory.get('trusted_apps'); return async (context, req, res) => { - const newTrustedApp = req.body; - try { - const exceptionsListService = exceptionListClientFromContext(context); - // Ensure list is created if it does not exist - await exceptionsListService.createTrustedAppsList(); - - const createdTrustedAppExceptionItem = await exceptionsListService.createExceptionListItem( - newTrustedAppItemToExceptionItem(newTrustedApp) - ); - return res.ok({ - body: { - data: exceptionItemToTrustedAppItem(createdTrustedAppExceptionItem), - }, + body: await createTrustedApp(exceptionListClientFromContext(context), req.body), }); } catch (error) { logger.error(error); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts new file mode 100644 index 0000000000000..2fd972ee402e2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts @@ -0,0 +1,425 @@ +/* + * 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 { CreateExceptionListItemOptions } from '../../../../../lists/server'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; + +import { + ConditionEntryField, + NewTrustedApp, + OperatingSystem, + TrustedApp, +} from '../../../../common/endpoint/types'; + +import { + createConditionEntry, + createEntryMatch, + createEntryNested, + exceptionListItemToTrustedApp, + newTrustedAppToCreateExceptionListItemOptions, +} from './mapping'; + +const createExceptionListItemOptions = ( + options: Partial +): CreateExceptionListItemOptions => ({ + comments: [], + description: '', + entries: [], + itemId: expect.any(String), + listId: 'endpoint_trusted_apps', + meta: undefined, + name: '', + namespaceType: 'agnostic', + osTypes: [], + tags: [], + type: 'simple', + ...options, +}); + +const exceptionListItemSchema = ( + item: Partial +): ExceptionListItemSchema => ({ + _version: '123', + id: '', + comments: [], + created_at: '', + created_by: '', + description: '', + entries: [], + item_id: '123', + list_id: 'endpoint_trusted_apps', + meta: undefined, + name: '', + namespace_type: 'agnostic', + os_types: [], + tags: [], + type: 'simple', + tie_breaker_id: '123', + updated_at: '11/11/2011T11:11:11.111', + updated_by: 'admin', + ...(item || {}), +}); + +describe('mapping', () => { + describe('newTrustedAppToCreateExceptionListItemOptions', () => { + const testMapping = (input: NewTrustedApp, expectedResult: CreateExceptionListItemOptions) => { + expect(newTrustedAppToCreateExceptionListItemOptions(input)).toEqual(expectedResult); + }; + + it('should map linux trusted app condition properly', function () { + testMapping( + { + name: 'linux trusted app', + description: 'Linux Trusted App', + os: OperatingSystem.LINUX, + entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + }, + createExceptionListItemOptions({ + name: 'linux trusted app', + description: 'Linux Trusted App', + osTypes: ['linux'], + entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + }) + ); + }); + + it('should map macos trusted app condition properly', function () { + testMapping( + { + name: 'macos trusted app', + description: 'MacOS Trusted App', + os: OperatingSystem.MAC, + entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + }, + createExceptionListItemOptions({ + name: 'macos trusted app', + description: 'MacOS Trusted App', + osTypes: ['macos'], + entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + }) + ); + }); + + it('should map windows trusted app condition properly', function () { + testMapping( + { + name: 'windows trusted app', + description: 'Windows Trusted App', + os: OperatingSystem.WINDOWS, + entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')], + }, + createExceptionListItemOptions({ + name: 'windows trusted app', + description: 'Windows Trusted App', + osTypes: ['windows'], + entries: [createEntryMatch('process.executable.caseless', 'C:\\Program Files\\Malware')], + }) + ); + }); + + it('should map signer condition properly', function () { + testMapping( + { + name: 'Signed trusted app', + description: 'Signed Trusted App', + os: OperatingSystem.WINDOWS, + entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')], + }, + createExceptionListItemOptions({ + name: 'Signed trusted app', + description: 'Signed Trusted App', + osTypes: ['windows'], + entries: [ + createEntryNested('process.Ext.code_signature', [ + createEntryMatch('trusted', 'true'), + createEntryMatch('subject_name', 'Microsoft Windows'), + ]), + ], + }) + ); + }); + + it('should map MD5 hash condition properly', function () { + testMapping( + { + name: 'MD5 trusted app', + description: 'MD5 Trusted App', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + ], + }, + createExceptionListItemOptions({ + name: 'MD5 trusted app', + description: 'MD5 Trusted App', + osTypes: ['linux'], + entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')], + }) + ); + }); + + it('should map SHA1 hash condition properly', function () { + testMapping( + { + name: 'SHA1 trusted app', + description: 'SHA1 Trusted App', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry( + ConditionEntryField.HASH, + 'f635da961234234659af249ddf3e40864e9fb241' + ), + ], + }, + createExceptionListItemOptions({ + name: 'SHA1 trusted app', + description: 'SHA1 Trusted App', + osTypes: ['linux'], + entries: [ + createEntryMatch('process.hash.sha1', 'f635da961234234659af249ddf3e40864e9fb241'), + ], + }) + ); + }); + + it('should map SHA256 hash condition properly', function () { + testMapping( + { + name: 'SHA256 trusted app', + description: 'SHA256 Trusted App', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry( + ConditionEntryField.HASH, + 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' + ), + ], + }, + createExceptionListItemOptions({ + name: 'SHA256 trusted app', + description: 'SHA256 Trusted App', + osTypes: ['linux'], + entries: [ + createEntryMatch( + 'process.hash.sha256', + 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' + ), + ], + }) + ); + }); + + it('should lowercase hash condition value', function () { + testMapping( + { + name: 'MD5 trusted app', + description: 'MD5 Trusted App', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.HASH, '1234234659Af249ddf3e40864E9FB241'), + ], + }, + createExceptionListItemOptions({ + name: 'MD5 trusted app', + description: 'MD5 Trusted App', + osTypes: ['linux'], + entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')], + }) + ); + }); + }); + + describe('exceptionListItemToTrustedApp', () => { + const testMapping = (input: ExceptionListItemSchema, expectedResult: TrustedApp) => { + expect(exceptionListItemToTrustedApp(input)).toEqual(expectedResult); + }; + + it('should map linux exception list item properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'linux trusted app', + description: 'Linux Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['linux'], + entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + }), + { + id: '123', + name: 'linux trusted app', + description: 'Linux Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os: OperatingSystem.LINUX, + entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + } + ); + }); + + it('should map macos exception list item properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'macos trusted app', + description: 'MacOS Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['macos'], + entries: [createEntryMatch('process.executable.caseless', '/bin/malware')], + }), + { + id: '123', + name: 'macos trusted app', + description: 'MacOS Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os: OperatingSystem.MAC, + entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')], + } + ); + }); + + it('should map windows exception list item properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'windows trusted app', + description: 'Windows Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['windows'], + entries: [createEntryMatch('process.executable.caseless', 'C:\\Program Files\\Malware')], + }), + { + id: '123', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + name: 'windows trusted app', + description: 'Windows Trusted App', + os: OperatingSystem.WINDOWS, + entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')], + } + ); + }); + + it('should map exception list item containing signer entry match properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'signed trusted app', + description: 'Signed trusted app', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['windows'], + entries: [ + createEntryNested('process.Ext.code_signature', [ + createEntryMatch('trusted', 'true'), + createEntryMatch('subject_name', 'Microsoft Windows'), + ]), + ], + }), + { + id: '123', + name: 'signed trusted app', + description: 'Signed trusted app', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os: OperatingSystem.WINDOWS, + entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')], + } + ); + }); + + it('should map exception list item containing MD5 hash entry match properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'MD5 trusted app', + description: 'MD5 Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['linux'], + entries: [createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241')], + }), + { + id: '123', + name: 'MD5 trusted app', + description: 'MD5 Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + ], + } + ); + }); + + it('should map exception list item containing SHA1 hash entry match properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'SHA1 trusted app', + description: 'SHA1 Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['linux'], + entries: [ + createEntryMatch('process.hash.sha1', 'f635da961234234659af249ddf3e40864e9fb241'), + ], + }), + { + id: '123', + name: 'SHA1 trusted app', + description: 'SHA1 Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry( + ConditionEntryField.HASH, + 'f635da961234234659af249ddf3e40864e9fb241' + ), + ], + } + ); + }); + + it('should map exception list item containing SHA256 hash entry match properly', function () { + testMapping( + exceptionListItemSchema({ + id: '123', + name: 'SHA256 trusted app', + description: 'SHA256 Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os_types: ['linux'], + entries: [ + createEntryMatch( + 'process.hash.sha256', + 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' + ), + ], + }), + { + id: '123', + name: 'SHA256 trusted app', + description: 'SHA256 Trusted App', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry( + ConditionEntryField.HASH, + 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' + ), + ], + } + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts new file mode 100644 index 0000000000000..1121e27e6e7bc --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -0,0 +1,185 @@ +/* + * 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 uuid from 'uuid'; + +import { OsType } from '../../../../../lists/common/schemas/common'; +import { + EntriesArray, + EntryMatch, + EntryNested, + ExceptionListItemSchema, + NestedEntriesArray, +} from '../../../../../lists/common/shared_exports'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; +import { CreateExceptionListItemOptions } from '../../../../../lists/server'; +import { + ConditionEntry, + ConditionEntryField, + NewTrustedApp, + OperatingSystem, + TrustedApp, +} from '../../../../common/endpoint/types'; + +type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry }; +type Mapping = { [K in T]: U }; + +const OS_TYPE_TO_OPERATING_SYSTEM: Mapping = { + linux: OperatingSystem.LINUX, + macos: OperatingSystem.MAC, + windows: OperatingSystem.WINDOWS, +}; + +const OPERATING_SYSTEM_TO_OS_TYPE: Mapping = { + [OperatingSystem.LINUX]: 'linux', + [OperatingSystem.MAC]: 'macos', + [OperatingSystem.WINDOWS]: 'windows', +}; + +const filterUndefined = (list: Array): T[] => { + return list.filter((item: T | undefined): item is T => item !== undefined); +}; + +export const createConditionEntry = ( + field: T, + value: string +): ConditionEntry => { + return { field, value, type: 'match', operator: 'included' }; +}; + +export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => { + return entries.reduce((result, entry) => { + if (entry.field.startsWith('process.hash') && entry.type === 'match') { + return { + ...result, + [ConditionEntryField.HASH]: createConditionEntry(ConditionEntryField.HASH, entry.value), + }; + } else if (entry.field === 'process.executable.caseless' && entry.type === 'match') { + return { + ...result, + [ConditionEntryField.PATH]: createConditionEntry(ConditionEntryField.PATH, entry.value), + }; + } else if (entry.field === 'process.Ext.code_signature' && entry.type === 'nested') { + const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => { + return subEntry.field === 'subject_name' && subEntry.type === 'match'; + }); + + if (subjectNameCondition) { + return { + ...result, + [ConditionEntryField.SIGNER]: createConditionEntry( + ConditionEntryField.SIGNER, + subjectNameCondition.value + ), + }; + } + } + + return result; + }, {} as ConditionEntriesMap); +}; + +/** + * Map an ExceptionListItem to a TrustedApp item + * @param exceptionListItem + */ +export const exceptionListItemToTrustedApp = ( + exceptionListItem: ExceptionListItemSchema +): TrustedApp => { + if (exceptionListItem.os_types[0]) { + const os = OS_TYPE_TO_OPERATING_SYSTEM[exceptionListItem.os_types[0]]; + const grouped = entriesToConditionEntriesMap(exceptionListItem.entries); + + return { + id: exceptionListItem.id, + name: exceptionListItem.name, + description: exceptionListItem.description, + created_at: exceptionListItem.created_at, + created_by: exceptionListItem.created_by, + ...(os === OperatingSystem.LINUX || os === OperatingSystem.MAC + ? { + os, + entries: filterUndefined([ + grouped[ConditionEntryField.HASH], + grouped[ConditionEntryField.PATH], + ]), + } + : { + os, + entries: filterUndefined([ + grouped[ConditionEntryField.HASH], + grouped[ConditionEntryField.PATH], + grouped[ConditionEntryField.SIGNER], + ]), + }), + }; + } else { + throw new Error('Unknown Operating System assigned to trusted application.'); + } +}; + +const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { + switch (hash.length) { + case 32: + return 'md5'; + case 40: + return 'sha1'; + case 64: + return 'sha256'; + } +}; + +export const createEntryMatch = (field: string, value: string): EntryMatch => { + return { field, value, type: 'match', operator: 'included' }; +}; + +export const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => { + return { field, entries, type: 'nested' }; +}; + +export const conditionEntriesToEntries = ( + conditionEntries: Array> +): EntriesArray => { + return conditionEntries.map((conditionEntry) => { + if (conditionEntry.field === ConditionEntryField.HASH) { + return createEntryMatch( + `process.hash.${hashType(conditionEntry.value)}`, + conditionEntry.value.toLowerCase() + ); + } else if (conditionEntry.field === ConditionEntryField.SIGNER) { + return createEntryNested(`process.Ext.code_signature`, [ + createEntryMatch('trusted', 'true'), + createEntryMatch('subject_name', conditionEntry.value), + ]); + } else { + return createEntryMatch(`process.executable.caseless`, conditionEntry.value); + } + }); +}; + +/** + * Map NewTrustedApp to CreateExceptionListItemOptions. + */ +export const newTrustedAppToCreateExceptionListItemOptions = ({ + os, + entries, + name, + description = '', +}: NewTrustedApp): CreateExceptionListItemOptions => { + return { + comments: [], + description, + entries: conditionEntriesToEntries(entries), + itemId: uuid.v4(), + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + meta: undefined, + name, + namespaceType: 'agnostic', + osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]], + tags: [], + type: 'simple', + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts new file mode 100644 index 0000000000000..04dec07f478e3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; +import { listMock } from '../../../../../lists/server/mocks'; +import { ExceptionListClient } from '../../../../../lists/server'; +import { ConditionEntryField, OperatingSystem } from '../../../../common/endpoint/types'; +import { createConditionEntry, createEntryMatch } from './mapping'; +import { + createTrustedApp, + deleteTrustedApp, + getTrustedAppsList, + MissingTrustedAppException, +} from './service'; + +const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; + +const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = { + _version: '123', + id: '123', + comments: [], + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + description: 'Linux trusted app 1', + entries: [ + createEntryMatch('process.executable.caseless', '/bin/malware'), + createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241'), + ], + item_id: '123', + list_id: 'endpoint_trusted_apps', + meta: undefined, + name: 'linux trusted app 1', + namespace_type: 'agnostic', + os_types: ['linux'], + tags: [], + type: 'simple', + tie_breaker_id: '123', + updated_at: '11/11/2011T11:11:11.111', + updated_by: 'admin', +}; + +const TRUSTED_APP = { + id: '123', + created_at: '11/11/2011T11:11:11.111', + created_by: 'admin', + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), + ], +}; + +describe('service', () => { + beforeEach(() => { + exceptionsListClient.deleteExceptionListItem.mockReset(); + exceptionsListClient.createExceptionListItem.mockReset(); + exceptionsListClient.findExceptionListItem.mockReset(); + exceptionsListClient.createTrustedAppsList.mockReset(); + }); + + describe('deleteTrustedApp', () => { + it('should delete existing trusted app', async () => { + exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); + + expect(await deleteTrustedApp(exceptionsListClient, { id: '123' })).toBeUndefined(); + + expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({ + id: '123', + namespaceType: 'agnostic', + }); + }); + + it('should throw for non existing trusted app', async () => { + exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null); + + await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf( + MissingTrustedAppException + ); + }); + }); + + describe('createTrustedApp', () => { + it('should create trusted app', async () => { + exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); + + const result = await createTrustedApp(exceptionsListClient, { + name: 'linux trusted app 1', + description: 'Linux trusted app 1', + os: OperatingSystem.LINUX, + entries: [ + createConditionEntry(ConditionEntryField.PATH, '/bin/malware'), + createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'), + ], + }); + + expect(result).toEqual({ data: TRUSTED_APP }); + + expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); + }); + }); + + describe('getTrustedAppsList', () => { + it('should get trusted apps', async () => { + exceptionsListClient.findExceptionListItem.mockResolvedValue({ + data: [EXCEPTION_LIST_ITEM], + page: 1, + per_page: 20, + total: 100, + }); + + const result = await getTrustedAppsList(exceptionsListClient, { page: 1, per_page: 20 }); + + expect(result).toEqual({ data: [TRUSTED_APP], page: 1, per_page: 20, total: 100 }); + + expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts new file mode 100644 index 0000000000000..f86c73f18a315 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionListClient } from '../../../../../lists/server'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; + +import { + DeleteTrustedAppsRequestParams, + GetTrustedAppsListRequest, + GetTrustedListAppsResponse, + PostTrustedAppCreateRequest, + PostTrustedAppCreateResponse, +} from '../../../../common/endpoint/types'; + +import { + exceptionListItemToTrustedApp, + newTrustedAppToCreateExceptionListItemOptions, +} from './mapping'; + +export class MissingTrustedAppException { + constructor(public id: string) {} +} + +export const deleteTrustedApp = async ( + exceptionsListClient: ExceptionListClient, + { id }: DeleteTrustedAppsRequestParams +) => { + const exceptionListItem = await exceptionsListClient.deleteExceptionListItem({ + id, + itemId: undefined, + namespaceType: 'agnostic', + }); + + if (!exceptionListItem) { + throw new MissingTrustedAppException(id); + } +}; + +export const getTrustedAppsList = async ( + exceptionsListClient: ExceptionListClient, + { page, per_page: perPage }: GetTrustedAppsListRequest +): Promise => { + // Ensure list is created if it does not exist + await exceptionsListClient.createTrustedAppsList(); + + const results = await exceptionsListClient.findExceptionListItem({ + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + page, + perPage, + filter: undefined, + namespaceType: 'agnostic', + sortField: 'name', + sortOrder: 'asc', + }); + + return { + data: results?.data.map(exceptionListItemToTrustedApp) ?? [], + total: results?.total ?? 0, + page: results?.page ?? 1, + per_page: results?.per_page ?? perPage!, + }; +}; + +export const createTrustedApp = async ( + exceptionsListClient: ExceptionListClient, + newTrustedApp: PostTrustedAppCreateRequest +): Promise => { + // Ensure list is created if it does not exist + await exceptionsListClient.createTrustedAppsList(); + + const createdTrustedAppExceptionItem = await exceptionsListClient.createExceptionListItem( + newTrustedAppToCreateExceptionListItemOptions(newTrustedApp) + ); + + return { data: exceptionListItemToTrustedApp(createdTrustedAppExceptionItem) }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts deleted file mode 100644 index 0fc469fa62a80..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { - createMockEndpointAppContext, - createMockEndpointAppContextServiceStartContract, -} from '../../mocks'; -import { IRouter, KibanaRequest, RequestHandler } from 'kibana/server'; -import { httpServerMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; -import { registerTrustedAppsRoutes } from './index'; -import { - TRUSTED_APPS_CREATE_API, - TRUSTED_APPS_DELETE_API, - TRUSTED_APPS_LIST_API, -} from '../../../../common/endpoint/constants'; -import { - DeleteTrustedAppsRequestParams, - GetTrustedAppsListRequest, - PostTrustedAppCreateRequest, -} from '../../../../common/endpoint/types'; -import { xpackMocks } from '../../../../../../mocks'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { EndpointAppContext } from '../../types'; -import { ExceptionListClient, ListClient } from '../../../../../lists/server'; -import { listMock } from '../../../../../lists/server/mocks'; -import { - ExceptionListItemSchema, - FoundExceptionListItemSchema, -} from '../../../../../lists/common/schemas/response'; -import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; - -type RequestHandlerContextWithLists = ReturnType & { - lists?: { - getListClient: () => jest.Mocked; - getExceptionListClient: () => jest.Mocked; - }; -}; - -describe('when invoking endpoint trusted apps route handlers', () => { - let routerMock: jest.Mocked; - let endpointAppContextService: EndpointAppContextService; - let context: RequestHandlerContextWithLists; - let response: ReturnType; - let exceptionsListClient: jest.Mocked; - let endpointAppContext: EndpointAppContext; - - beforeEach(() => { - routerMock = httpServiceMock.createRouter(); - endpointAppContextService = new EndpointAppContextService(); - const startContract = createMockEndpointAppContextServiceStartContract(); - exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; - endpointAppContextService.start(startContract); - endpointAppContext = { - ...createMockEndpointAppContext(), - service: endpointAppContextService, - }; - registerTrustedAppsRoutes(routerMock, endpointAppContext); - - // For use in individual API calls - context = { - ...xpackMocks.createRequestHandlerContext(), - lists: { - getListClient: jest.fn(), - getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient), - }, - }; - response = httpServerMock.createResponseFactory(); - }); - - describe('when fetching list of trusted apps', () => { - let routeHandler: RequestHandler; - const createListRequest = (page: number = 1, perPage: number = 20) => { - return httpServerMock.createKibanaRequest({ - path: TRUSTED_APPS_LIST_API, - method: 'get', - query: { - page, - per_page: perPage, - }, - }); - }; - - beforeEach(() => { - // Get the registered List handler from the IRouter instance - [, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(TRUSTED_APPS_LIST_API) - )!; - }); - - it('should use ExceptionListClient from route handler context', async () => { - const request = createListRequest(); - await routeHandler(context, request, response); - expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); - }); - - it('should create the Trusted Apps List first', async () => { - const request = createListRequest(); - await routeHandler(context, request, response); - expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); - expect(response.ok).toHaveBeenCalled(); - }); - - it('should pass pagination query params to exception list service', async () => { - const request = createListRequest(10, 100); - const emptyResponse = { - data: [], - page: 10, - per_page: 100, - total: 0, - }; - - exceptionsListClient.findExceptionListItem.mockResolvedValue(emptyResponse); - await routeHandler(context, request, response); - - expect(response.ok).toHaveBeenCalledWith({ body: emptyResponse }); - expect(exceptionsListClient.findExceptionListItem).toHaveBeenCalledWith({ - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - page: 10, - perPage: 100, - filter: undefined, - namespaceType: 'agnostic', - sortField: 'name', - sortOrder: 'asc', - }); - }); - - it('should map Exception List Item to Trusted App item', async () => { - const request = createListRequest(10, 100); - const emptyResponse: FoundExceptionListItemSchema = { - data: [ - { - _version: undefined, - comments: [], - created_at: '2020-09-21T19:43:48.240Z', - created_by: 'test', - description: '', - entries: [ - { - field: 'process.hash.sha256', - operator: 'included', - type: 'match', - value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', - }, - { - field: 'process.hash.sha1', - operator: 'included', - type: 'match', - value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', - }, - { - field: 'process.hash.md5', - operator: 'included', - type: 'match', - value: '741462ab431a22233c787baab9b653c7', - }, - ], - id: '1', - item_id: '11', - list_id: 'trusted apps test', - meta: undefined, - name: 'test', - namespace_type: 'agnostic', - os_types: ['windows'], - tags: [], - tie_breaker_id: '1', - type: 'simple', - updated_at: '2020-09-21T19:43:48.240Z', - updated_by: 'test', - }, - ], - page: 10, - per_page: 100, - total: 0, - }; - - exceptionsListClient.findExceptionListItem.mockResolvedValue(emptyResponse); - await routeHandler(context, request, response); - - expect(response.ok).toHaveBeenCalledWith({ - body: { - data: [ - { - created_at: '2020-09-21T19:43:48.240Z', - created_by: 'test', - description: '', - entries: [ - { - field: 'process.hash.*', - operator: 'included', - type: 'match', - value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', - }, - { - field: 'process.hash.*', - operator: 'included', - type: 'match', - value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', - }, - { - field: 'process.hash.*', - operator: 'included', - type: 'match', - value: '741462ab431a22233c787baab9b653c7', - }, - ], - id: '1', - name: 'test', - os: 'windows', - }, - ], - page: 10, - per_page: 100, - total: 0, - }, - }); - }); - - it('should log unexpected error if one occurs', async () => { - exceptionsListClient.findExceptionListItem.mockImplementation(() => { - throw new Error('expected error'); - }); - const request = createListRequest(10, 100); - await routeHandler(context, request, response); - expect(response.internalError).toHaveBeenCalled(); - expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); - }); - }); - - describe('when creating a trusted app', () => { - let routeHandler: RequestHandler; - const createNewTrustedAppBody = (): { - -readonly [k in keyof PostTrustedAppCreateRequest]: PostTrustedAppCreateRequest[k]; - } => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: 'windows', - entries: [ - { - field: 'process.executable.caseless', - type: 'match', - operator: 'included', - value: 'c:/programs files/Anti-Virus', - }, - ], - }); - const createPostRequest = (body?: PostTrustedAppCreateRequest) => { - return httpServerMock.createKibanaRequest({ - path: TRUSTED_APPS_LIST_API, - method: 'post', - body: body ?? createNewTrustedAppBody(), - }); - }; - - beforeEach(() => { - // Get the registered POST handler from the IRouter instance - [, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(TRUSTED_APPS_CREATE_API) - )!; - - // Mock the impelementation of `createExceptionListItem()` so that the return value - // merges in the provided input - exceptionsListClient.createExceptionListItem.mockImplementation(async (newExceptionItem) => { - return ({ - ...getExceptionListItemSchemaMock(), - ...newExceptionItem, - os_types: newExceptionItem.osTypes, - } as unknown) as ExceptionListItemSchema; - }); - }); - - it('should use ExceptionListClient from route handler context', async () => { - const request = createPostRequest(); - await routeHandler(context, request, response); - expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); - }); - - it('should create trusted app list first', async () => { - const request = createPostRequest(); - await routeHandler(context, request, response); - expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); - expect(response.ok).toHaveBeenCalled(); - }); - - it('should map new trusted app item to an exception list item', async () => { - const request = createPostRequest(); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0]).toEqual({ - comments: [], - description: 'this one is ok', - entries: [ - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: 'c:/programs files/Anti-Virus', - }, - ], - itemId: expect.stringMatching(/.*/), - listId: 'endpoint_trusted_apps', - meta: undefined, - name: 'Some Anti-Virus App', - namespaceType: 'agnostic', - osTypes: ['windows'], - tags: [], - type: 'simple', - }); - }); - - it('should return new trusted app item', async () => { - const request = createPostRequest(); - await routeHandler(context, request, response); - expect(response.ok.mock.calls[0][0]).toEqual({ - body: { - data: { - created_at: '2020-04-20T15:25:31.830Z', - created_by: 'some user', - description: 'this one is ok', - entries: [ - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: 'c:/programs files/Anti-Virus', - }, - ], - id: '1', - name: 'Some Anti-Virus App', - os: 'windows', - }, - }, - }); - }); - - it('should log unexpected error if one occurs', async () => { - exceptionsListClient.createExceptionListItem.mockImplementation(() => { - throw new Error('expected error for create'); - }); - const request = createPostRequest(); - await routeHandler(context, request, response); - expect(response.internalError).toHaveBeenCalled(); - expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); - }); - - it('should trim trusted app entry name', async () => { - const newTrustedApp = createNewTrustedAppBody(); - newTrustedApp.name = `\n ${newTrustedApp.name} \r\n`; - const request = createPostRequest(newTrustedApp); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].name).toEqual( - 'Some Anti-Virus App' - ); - }); - - it('should trim condition entry values', async () => { - const newTrustedApp = createNewTrustedAppBody(); - newTrustedApp.entries.push({ - field: 'process.executable.caseless', - value: '\n some value \r\n ', - operator: 'included', - type: 'match', - }); - const request = createPostRequest(newTrustedApp); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: 'c:/programs files/Anti-Virus', - }, - { - field: 'process.executable.caseless', - value: 'some value', - operator: 'included', - type: 'match', - }, - ]); - }); - - it('should convert hash values to lowercase', async () => { - const newTrustedApp = createNewTrustedAppBody(); - newTrustedApp.entries.push({ - field: 'process.hash.*', - value: '741462AB431A22233C787BAAB9B653C7', - operator: 'included', - type: 'match', - }); - const request = createPostRequest(newTrustedApp); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: 'c:/programs files/Anti-Virus', - }, - { - field: 'process.hash.md5', - value: '741462ab431a22233c787baab9b653c7', - operator: 'included', - type: 'match', - }, - ]); - }); - - it('should detect md5 hash', async () => { - const newTrustedApp = createNewTrustedAppBody(); - newTrustedApp.entries = [ - { - field: 'process.hash.*', - value: '741462ab431a22233c787baab9b653c7', - operator: 'included', - type: 'match', - }, - ]; - const request = createPostRequest(newTrustedApp); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ - { - field: 'process.hash.md5', - value: '741462ab431a22233c787baab9b653c7', - operator: 'included', - type: 'match', - }, - ]); - }); - - it('should detect sha1 hash', async () => { - const newTrustedApp = createNewTrustedAppBody(); - newTrustedApp.entries = [ - { - field: 'process.hash.*', - value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', - operator: 'included', - type: 'match', - }, - ]; - const request = createPostRequest(newTrustedApp); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ - { - field: 'process.hash.sha1', - value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', - operator: 'included', - type: 'match', - }, - ]); - }); - - it('should detect sha256 hash', async () => { - const newTrustedApp = createNewTrustedAppBody(); - newTrustedApp.entries = [ - { - field: 'process.hash.*', - value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', - operator: 'included', - type: 'match', - }, - ]; - const request = createPostRequest(newTrustedApp); - await routeHandler(context, request, response); - expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ - { - field: 'process.hash.sha256', - value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', - operator: 'included', - type: 'match', - }, - ]); - }); - }); - - describe('when deleting a trusted app', () => { - let routeHandler: RequestHandler; - let request: KibanaRequest; - - beforeEach(() => { - [, routeHandler] = routerMock.delete.mock.calls.find(([{ path }]) => - path.startsWith(TRUSTED_APPS_DELETE_API) - )!; - - request = httpServerMock.createKibanaRequest({ - path: TRUSTED_APPS_DELETE_API.replace('{id}', '123'), - method: 'delete', - }); - }); - - it('should use ExceptionListClient from route handler context', async () => { - await routeHandler(context, request, response); - expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); - }); - - it('should return 200 on successful delete', async () => { - await routeHandler(context, request, response); - expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({ - id: request.params.id, - itemId: undefined, - namespaceType: 'agnostic', - }); - expect(response.ok).toHaveBeenCalled(); - }); - - it('should return 404 if item does not exist', async () => { - exceptionsListClient.deleteExceptionListItem.mockResolvedValueOnce(null); - await routeHandler(context, request, response); - expect(response.notFound).toHaveBeenCalled(); - }); - - it('should log unexpected error if one occurs', async () => { - exceptionsListClient.deleteExceptionListItem.mockImplementation(() => { - throw new Error('expected error for delete'); - }); - await routeHandler(context, request, response); - expect(response.internalError).toHaveBeenCalled(); - expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts deleted file mode 100644 index 322d9a65162c0..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import uuid from 'uuid'; -import { ExceptionListItemSchema } from '../../../../../lists/common/shared_exports'; -import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types'; -import { ExceptionListClient } from '../../../../../lists/server'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; - -type NewExceptionItem = Parameters[0]; - -/** - * Map an ExcptionListItem to a TrustedApp item - * @param exceptionListItem - */ -export const exceptionItemToTrustedAppItem = ( - exceptionListItem: ExceptionListItemSchema -): TrustedApp => { - // eslint-disable-next-line @typescript-eslint/naming-convention - 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')) { - return { - ...entry, - field: 'process.hash.*', - }; - } - return entry; - }), - description, - created_at, - created_by, - name, - os, - id, - } as TrustedApp; -}; - -export const newTrustedAppItemToExceptionItem = ({ - os, - entries, - name, - description = '', -}: NewTrustedApp): NewExceptionItem => { - return { - comments: [], - description, - // @ts-ignore - entries: entries.map(({ value, ...newEntry }) => { - let newValue = value.trim(); - - if (newEntry.field === 'process.hash.*') { - newValue = newValue.toLowerCase(); - newEntry.field = `process.hash.${hashType(newValue)}`; - } - - return { - ...newEntry, - value: newValue, - }; - }), - itemId: uuid.v4(), - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - meta: undefined, - name: name.trim(), - namespaceType: 'agnostic', - osTypes: [os], - tags: [], - type: 'simple', - }; -}; - -const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { - switch (hash.length) { - case 32: - return 'md5'; - case 40: - return 'sha1'; - case 64: - return 'sha256'; - } -};