diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 507ce63c7b815..b72a52f0a0eb7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -13,3 +13,4 @@ export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurre export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; +export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; 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 7aec8e15c317c..b0c769216732d 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetTrustedAppsRequestSchema } from './trusted_apps'; +import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps'; describe('When invoking Trusted Apps Schema', () => { describe('for GET List', () => { @@ -68,4 +68,180 @@ describe('When invoking Trusted Apps Schema', () => { }); }); }); + + describe('for POST Create', () => { + const getCreateTrustedAppItem = () => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: 'windows', + entries: [ + { + field: 'path', + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + }, + ], + }); + const body = PostTrustedAppCreateRequestSchema.body; + + it('should not error on a valid message', () => { + const bodyMsg = getCreateTrustedAppItem(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `name` is required', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + name: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `name` value to be non-empty', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + name: '', + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `description` as optional', () => { + const { description, ...bodyMsg } = getCreateTrustedAppItem(); + expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); + }); + + it('should validate `description` to be non-empty if defined', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + description: '', + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `os` to to only accept known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + os: undefined, + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const bodyMsg2 = { + ...bodyMsg, + os: '', + }; + expect(() => body.validate(bodyMsg2)).toThrow(); + + const bodyMsg3 = { + ...bodyMsg, + os: 'winz', + }; + expect(() => body.validate(bodyMsg3)).toThrow(); + + ['linux', 'macos', '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(); + + const { entries, ...bodyMsg2 } = getCreateTrustedAppItem(); + expect(() => body.validate(bodyMsg2)).toThrow(); + }); + + it('should validate `entries` to have at least 1 item', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + describe('when `entries` are defined', () => { + const getTrustedAppItemEntryItem = () => getCreateTrustedAppItem().entries[0]; + + it('should validate `entry.field` is required', () => { + const { field, ...entry } = getTrustedAppItemEntryItem(); + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [entry], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.field` is limited to known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field: '', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + const bodyMsg2 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field: 'invalid value', + }, + ], + }; + expect(() => body.validate(bodyMsg2)).toThrow(); + + ['hash', 'path'].forEach((field) => { + const bodyMsg3 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + field, + }, + ], + }; + + expect(() => body.validate(bodyMsg3)).not.toThrow(); + }); + }); + + it.todo('should validate `entry.type` is limited to known values'); + + it.todo('should validate `entry.operator` is limited to known values'); + + it('should validate `entry.value` required', () => { + const { value, ...entry } = getTrustedAppItemEntryItem(); + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [entry], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + }); + + it('should validate `entry.value` is non-empty', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + value: '', + }, + ], + }; + expect(() => body.validate(bodyMsg)).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 20fab93aaf304..7535b23a10e8a 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 @@ -12,3 +12,20 @@ export const GetTrustedAppsRequestSchema = { per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), }), }; + +export const PostTrustedAppCreateRequestSchema = { + body: schema.object({ + name: schema.string({ minLength: 1 }), + description: schema.maybe(schema.string({ minLength: 1 })), + os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), + entries: schema.arrayOf( + schema.object({ + field: schema.oneOf([schema.literal('hash'), schema.literal('path')]), + type: schema.literal('match'), + operator: schema.literal('included'), + value: schema.string({ minLength: 1 }), + }), + { minSize: 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 2905274bef1cb..7aeb6c6024b99 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 @@ -5,7 +5,10 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { GetTrustedAppsRequestSchema } from '../schema/trusted_apps'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, +} from '../schema/trusted_apps'; /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; @@ -16,6 +19,12 @@ export interface GetTrustedListAppsResponse { data: TrustedApp[]; } +/** API Request body for creating a new Trusted App entry */ +export type PostTrustedAppCreateRequest = TypeOf; +export interface PostTrustedAppCreateResponse { + data: TrustedApp; +} + interface MacosLinuxConditionEntry { field: 'hash' | 'path'; type: 'match'; 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 6c29a2244c203..977683ab55495 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 @@ -8,9 +8,10 @@ import { RequestHandler } from 'kibana/server'; import { GetTrustedAppsListRequest, GetTrustedListAppsResponse, + PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; -import { exceptionItemToTrustedAppItem } from './utils'; +import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; export const getTrustedAppsListRouteHandler = ( @@ -24,7 +25,7 @@ export const getTrustedAppsListRouteHandler = ( try { // Ensure list is created if it does not exist - await exceptionsListService?.createTrustedAppsList(); + await exceptionsListService.createTrustedAppsList(); const results = await exceptionsListService.findExceptionListItem({ listId: ENDPOINT_TRUSTED_APPS_LIST_ID, page, @@ -47,3 +48,32 @@ export const getTrustedAppsListRouteHandler = ( } }; }; + +export const getTrustedAppsCreateRouteHandler = ( + endpointAppContext: EndpointAppContext +): RequestHandler => { + const logger = endpointAppContext.logFactory.get('trusted_apps'); + + return async (constext, req, res) => { + const exceptionsListService = endpointAppContext.service.getExceptionsList(); + const newTrustedApp = req.body; + + try { + // 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), + }, + }); + } catch (error) { + logger.error(error); + return res.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index 178aa06eee877..1302b10533ccf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -5,9 +5,15 @@ */ import { IRouter } from 'kibana/server'; -import { GetTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; -import { TRUSTED_APPS_LIST_API } from '../../../../common/endpoint/constants'; -import { getTrustedAppsListRouteHandler } from './handlers'; +import { + GetTrustedAppsRequestSchema, + PostTrustedAppCreateRequestSchema, +} from '../../../../common/endpoint/schema/trusted_apps'; +import { + TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_LIST_API, +} from '../../../../common/endpoint/constants'; +import { getTrustedAppsCreateRouteHandler, getTrustedAppsListRouteHandler } from './handlers'; import { EndpointAppContext } from '../../types'; export const registerTrustedAppsRoutes = ( @@ -23,4 +29,14 @@ export const registerTrustedAppsRoutes = ( }, getTrustedAppsListRouteHandler(endpointAppContext) ); + + // CREATE + router.post( + { + path: TRUSTED_APPS_CREATE_API, + validate: PostTrustedAppCreateRequestSchema, + options: { authRequired: true }, + }, + getTrustedAppsCreateRouteHandler(endpointAppContext) + ); }; 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 1d4a7919b89f5..488c8390411b0 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 @@ -12,12 +12,20 @@ import { import { IRouter, RequestHandler } from 'kibana/server'; import { httpServerMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; import { registerTrustedAppsRoutes } from './index'; -import { TRUSTED_APPS_LIST_API } from '../../../../common/endpoint/constants'; -import { GetTrustedAppsListRequest } from '../../../../common/endpoint/types'; +import { + TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_LIST_API, +} from '../../../../common/endpoint/constants'; +import { + 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 } from '../../../../../lists/server'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; describe('when invoking endpoint trusted apps route handlers', () => { let routerMock: jest.Mocked; @@ -105,4 +113,111 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); }); + + describe('when creating a trusted app', () => { + let routeHandler: RequestHandler; + const createNewTrustedAppBody = (): PostTrustedAppCreateRequest => ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: 'windows', + entries: [ + { + field: 'path', + type: 'match', + operator: 'included', + value: 'c:/programs files/Anti-Virus', + }, + ], + }); + const createPostRequest = () => { + return httpServerMock.createKibanaRequest({ + path: TRUSTED_APPS_LIST_API, + method: 'post', + 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, + } as unknown) as ExceptionListItemSchema; + }); + }); + + 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({ + _tags: ['os:windows'], + comments: [], + description: 'this one is ok', + entries: [ + { + field: 'path', + 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', + 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: 'path', + 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(); + }); + }); }); 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 2b417a4c6a8e1..794c1db4b49aa 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 @@ -4,8 +4,13 @@ * 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 { TrustedApp } from '../../../../common/endpoint/types'; +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]; /** * Map an ExcptionListItem to a TrustedApp item @@ -40,3 +45,28 @@ const osFromTagsList = (tags: string[]): TrustedApp['os'] | 'unknown' => { } return 'unknown'; }; + +export const newTrustedAppItemToExceptionItem = ({ + os, + entries, + name, + description = '', +}: NewTrustedApp): NewExecptionItem => { + return { + _tags: tagsListFromOs(os), + comments: [], + description, + entries, + itemId: uuid.v4(), + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + meta: undefined, + name, + namespaceType: 'agnostic', + tags: [], + type: 'simple', + }; +}; + +const tagsListFromOs = (os: NewTrustedApp['os']): NewExecptionItem['_tags'] => { + return [`os:${os}`]; +};