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 fc94e9a7c312a5..c0fbebf73ed8af 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 @@ -76,7 +76,7 @@ describe('When invoking Trusted Apps Schema', () => { os: 'windows', entries: [ { - field: 'process.path', + field: 'process.path.text', type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', @@ -194,7 +194,7 @@ describe('When invoking Trusted Apps Schema', () => { }; expect(() => body.validate(bodyMsg2)).toThrow(); - ['process.hash.*', 'process.path'].forEach((field) => { + ['process.hash.*', 'process.path.text'].forEach((field) => { const bodyMsg3 = { ...getCreateTrustedAppItem(), entries: [ 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 72e24a7d694d4c..3b3bec4a478046 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 @@ -26,7 +26,10 @@ export const PostTrustedAppCreateRequestSchema = { os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), entries: schema.arrayOf( schema.object({ - field: schema.oneOf([schema.literal('process.hash.*'), schema.literal('process.path')]), + field: schema.oneOf([ + schema.literal('process.hash.*'), + schema.literal('process.path.text'), + ]), type: schema.literal('match'), operator: schema.literal('included'), value: schema.string({ minLength: 1 }), 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 3356fc67d26820..93e3305078f8d2 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 @@ -12,6 +12,7 @@ import { /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; + export interface GetTrustedListAppsResponse { per_page: number; page: number; @@ -21,12 +22,13 @@ export interface GetTrustedListAppsResponse { /** API Request body for creating a new Trusted App entry */ export type PostTrustedAppCreateRequest = TypeOf; + export interface PostTrustedAppCreateResponse { data: TrustedApp; } export interface MacosLinuxConditionEntry { - field: 'process.hash.*' | 'process.path'; + field: 'process.hash.*' | 'process.path.text'; type: 'match'; operator: 'included'; value: string; 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 23bced0c048b19..7eeadeb02a3852 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 @@ -76,7 +76,7 @@ export const ConditionEntry = memo( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path', { defaultMessage: 'Path' } ), - value: 'process.path', + value: 'process.path.text', }, ]; }, []); 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 index 35d0bf1116148e..2368dcda09a38e 100644 --- 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 @@ -26,7 +26,10 @@ import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/const import { EndpointAppContext } from '../../types'; import { ExceptionListClient, ListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; -import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; +import { + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '../../../../../lists/common/schemas/response'; import { DeleteTrustedAppsRequestParams } from './types'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -125,6 +128,97 @@ describe('when invoking endpoint trusted apps route handlers', () => { }); }); + it('should map Exception List Item to Trusted App item', async () => { + const request = createListRequest(10, 100); + const emptyResponse: FoundExceptionListItemSchema = { + data: [ + { + _tags: ['os:windows'], + _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', + 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'); @@ -138,24 +232,26 @@ describe('when invoking endpoint trusted apps route handlers', () => { describe('when creating a trusted app', () => { let routeHandler: RequestHandler; - const createNewTrustedAppBody = (): PostTrustedAppCreateRequest => ({ + const createNewTrustedAppBody = (): { + -readonly [k in keyof PostTrustedAppCreateRequest]: PostTrustedAppCreateRequest[k]; + } => ({ name: 'Some Anti-Virus App', description: 'this one is ok', os: 'windows', entries: [ { - field: 'process.path', + field: 'process.path.text', type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', }, ], }); - const createPostRequest = () => { + const createPostRequest = (body?: PostTrustedAppCreateRequest) => { return httpServerMock.createKibanaRequest({ path: TRUSTED_APPS_LIST_API, method: 'post', - body: createNewTrustedAppBody(), + body: body ?? createNewTrustedAppBody(), }); }; @@ -197,7 +293,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { description: 'this one is ok', entries: [ { - field: 'process.path', + field: 'process.path.text', operator: 'included', type: 'match', value: 'c:/programs files/Anti-Virus', @@ -224,7 +320,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { description: 'this one is ok', entries: [ { - field: 'process.path', + field: 'process.path.text', operator: 'included', type: 'match', value: 'c:/programs files/Anti-Virus', @@ -247,6 +343,134 @@ describe('when invoking endpoint trusted apps route handlers', () => { 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.path.text', + 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.path.text', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + { + field: 'process.path.text', + 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.path.text', + 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', () => { 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 index 794c1db4b49aa8..2b8129ab950c66 100644 --- 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 @@ -10,7 +10,7 @@ import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -type NewExecptionItem = Parameters[0]; +type NewExceptionItem = Parameters[0]; /** * Map an ExcptionListItem to a TrustedApp item @@ -23,7 +23,15 @@ export const exceptionItemToTrustedAppItem = ( const { entries, description, created_by, created_at, name, _tags, id } = exceptionListItem; const os = osFromTagsList(_tags); return { - entries, + entries: entries.map((entry) => { + if (entry.field.startsWith('process.hash')) { + return { + ...entry, + field: 'process.hash.*', + }; + } + return entry; + }), description, created_at, created_by, @@ -51,22 +59,46 @@ export const newTrustedAppItemToExceptionItem = ({ entries, name, description = '', -}: NewTrustedApp): NewExecptionItem => { +}: NewTrustedApp): NewExceptionItem => { return { _tags: tagsListFromOs(os), comments: [], description, - entries, + // @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: name.trim(), namespaceType: 'agnostic', tags: [], type: 'simple', }; }; -const tagsListFromOs = (os: NewTrustedApp['os']): NewExecptionItem['_tags'] => { +const tagsListFromOs = (os: NewTrustedApp['os']): NewExceptionItem['_tags'] => { return [`os:${os}`]; }; + +const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { + switch (hash.length) { + case 32: + return 'md5'; + case 40: + return 'sha1'; + case 64: + return 'sha256'; + } +};