From 1c0a7932040c89b37b9387665c6d639e9417b1b4 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 27 Aug 2020 08:04:50 -0400 Subject: [PATCH] [SECURITY_SOLUTION][ENDPOINT] Trusted Apps List API (#75476) (#76029) * Trusted Apps API for retrieving list * Added types for TrustedApp entries * generator for trusted apps entries * Add mocked `createTrustedAppsList()` method to `ExceptionListClientMock` --- .../response/exception_list_schema.mock.ts | 14 +++ .../exception_list_client.mock.ts | 6 +- .../common/endpoint/constants.ts | 2 + .../endpoint/schema/trusted_apps.test.ts | 71 ++++++++++++ .../common/endpoint/schema/trusted_apps.ts | 14 +++ .../endpoint/{types.ts => types/index.ts} | 6 +- .../common/endpoint/types/trusted_apps.ts | 52 +++++++++ .../scripts/endpoint/load_trusted_apps.js | 9 ++ .../scripts/endpoint/trusted_apps/index.ts | 91 +++++++++++++++ .../endpoint/endpoint_app_context_services.ts | 11 ++ .../server/endpoint/mocks.ts | 2 + .../endpoint/routes/trusted_apps/handlers.ts | 49 ++++++++ .../endpoint/routes/trusted_apps/index.ts | 26 +++++ .../routes/trusted_apps/trusted_apps.test.ts | 108 ++++++++++++++++++ .../endpoint/routes/trusted_apps/utils.ts | 42 +++++++ .../security_solution/server/plugin.ts | 3 + 16 files changed, 503 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts rename x-pack/plugins/security_solution/common/endpoint/{types.ts => types/index.ts} (99%) create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts create mode 100755 x-pack/plugins/security_solution/scripts/endpoint/load_trusted_apps.js create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index e2f0a7c06b400..6df051e83b97c 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -19,6 +19,11 @@ import { _VERSION, } from '../../constants.mock'; import { ENDPOINT_LIST_ID } from '../..'; +import { + ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, + ENDPOINT_TRUSTED_APPS_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_NAME, +} from '../../constants'; import { ExceptionListSchema } from './exception_list_schema'; @@ -42,6 +47,15 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ version: VERSION, }); +export const getTrustedAppsListSchemaMock = (): ExceptionListSchema => { + return { + ...getExceptionListSchemaMock(), + description: ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: ENDPOINT_TRUSTED_APPS_LIST_NAME, + }; +}; + /** * This is useful for end to end tests where we remove the auto generated parts for comparisons * such as created_at, updated_at, and id. diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts index d0e238f8c5c40..4354c735747be 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts @@ -9,7 +9,10 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { getFoundExceptionListSchemaMock } from '../../../common/schemas/response/found_exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; -import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; +import { + getExceptionListSchemaMock, + getTrustedAppsListSchemaMock, +} from '../../../common/schemas/response/exception_list_schema.mock'; import { ExceptionListClient } from './exception_list_client'; @@ -24,6 +27,7 @@ export class ExceptionListClientMock extends ExceptionListClient { public deleteExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()); public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock()); + public createTrustedAppsList = jest.fn().mockResolvedValue(getTrustedAppsListSchemaMock()); } export const getExceptionListClientMock = (): ExceptionListClient => { diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 6ea0c36328eed..507ce63c7b815 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -11,3 +11,5 @@ export const policyIndexPattern = 'metrics-endpoint.policy-*'; 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_LIST_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 new file mode 100644 index 0000000000000..7aec8e15c317c --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { GetTrustedAppsRequestSchema } from './trusted_apps'; + +describe('When invoking Trusted Apps Schema', () => { + describe('for GET List', () => { + const getListQueryParams = (page: unknown = 1, perPage: unknown = 20) => ({ + page, + per_page: perPage, + }); + const query = GetTrustedAppsRequestSchema.query; + + describe('query param validation', () => { + it('should return query params if valid', () => { + expect(query.validate(getListQueryParams())).toEqual({ + page: 1, + per_page: 20, + }); + }); + + it('should use default values', () => { + expect(query.validate(getListQueryParams(undefined, undefined))).toEqual({ + page: 1, + per_page: 20, + }); + expect(query.validate(getListQueryParams(undefined, 100))).toEqual({ + page: 1, + per_page: 100, + }); + expect(query.validate(getListQueryParams(10, undefined))).toEqual({ + page: 10, + per_page: 20, + }); + }); + + it('should throw if `page` param is not a number', () => { + expect(() => { + query.validate(getListQueryParams('one')); + }).toThrowError(); + }); + + it('should throw if `page` param is less than 1', () => { + expect(() => { + query.validate(getListQueryParams(0)); + }).toThrowError(); + expect(() => { + query.validate(getListQueryParams(-1)); + }).toThrowError(); + }); + + it('should throw if `per_page` param is not a number', () => { + expect(() => { + query.validate(getListQueryParams(1, 'twenty')); + }).toThrowError(); + }); + + it('should throw if `per_page` param is less than 1', () => { + expect(() => { + query.validate(getListQueryParams(1, 0)); + }).toThrowError(); + expect(() => { + query.validate(getListQueryParams(1, -1)); + }).toThrowError(); + }); + }); + }); +}); 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 new file mode 100644 index 0000000000000..20fab93aaf304 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -0,0 +1,14 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const GetTrustedAppsRequestSchema = { + query: schema.object({ + page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), + per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts similarity index 99% rename from x-pack/plugins/security_solution/common/endpoint/types.ts rename to x-pack/plugins/security_solution/common/endpoint/types/index.ts index 2b8de7ed16b08..8e507cbc921a2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -5,8 +5,10 @@ */ import { ApplicationStart } from 'kibana/public'; -import { NewPackagePolicy, PackagePolicy } from '../../../ingest_manager/common'; -import { ManifestSchema } from './schema/manifest'; +import { NewPackagePolicy, PackagePolicy } from '../../../../ingest_manager/common'; +import { ManifestSchema } from '../schema/manifest'; + +export * from './trusted_apps'; /** * Supported React-Router state for the Policy Details page 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 new file mode 100644 index 0000000000000..2905274bef1cb --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -0,0 +1,52 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { GetTrustedAppsRequestSchema } from '../schema/trusted_apps'; + +/** API request params for retrieving a list of Trusted Apps */ +export type GetTrustedAppsListRequest = TypeOf; +export interface GetTrustedListAppsResponse { + per_page: number; + page: number; + total: number; + data: TrustedApp[]; +} + +interface MacosLinuxConditionEntry { + field: 'hash' | 'path'; + type: 'match'; + operator: 'included'; + value: string; +} + +type WindowsConditionEntry = + | MacosLinuxConditionEntry + | (Omit & { + field: 'signer'; + }); + +/** Type for a new Trusted App Entry */ +export type NewTrustedApp = { + name: string; + description?: string; +} & ( + | { + os: 'linux' | 'macos'; + entries: MacosLinuxConditionEntry[]; + } + | { + os: 'windows'; + entries: WindowsConditionEntry[]; + } +); + +/** A trusted app entry */ +export type TrustedApp = NewTrustedApp & { + id: string; + created_at: string; + created_by: string; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/load_trusted_apps.js b/x-pack/plugins/security_solution/scripts/endpoint/load_trusted_apps.js new file mode 100755 index 0000000000000..872639dd9fb7e --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/load_trusted_apps.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/* + * 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. + */ + +require('../../../../../src/setup_node_env'); +require('./trusted_apps').cli(); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts new file mode 100644 index 0000000000000..3bd27259ad80c --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts @@ -0,0 +1,91 @@ +/* + * 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 { v4 as generateUUID } from 'uuid'; +// @ts-ignore +import minimist from 'minimist'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../lists/common/constants'; +import { TRUSTED_APPS_LIST_API } from '../../../common/endpoint/constants'; +import { ExceptionListItemSchema } from '../../../../lists/common/schemas/response'; + +interface RunOptions { + count?: number; +} + +const logger = new ToolingLog({ level: 'info', writeTo: process.stdout }); +const separator = '----------------------------------------'; + +export const cli = async () => { + const options: RunOptions = minimist(process.argv.slice(2), { + default: { + count: 10, + }, + }); + logger.write(`${separator} +Loading ${options.count} Trusted App Entries`); + await run(options); + logger.write(`Done! +${separator}`); +}; + +export const run: (options?: RunOptions) => Promise = async ({ + count = 10, +}: RunOptions = {}) => { + const kbnClient = new KbnClient(logger, { url: 'http://elastic:changeme@localhost:5601' }); + + // touch the Trusted Apps List so it can be created + await kbnClient.request({ + method: 'GET', + path: TRUSTED_APPS_LIST_API, + }); + + return Promise.all( + Array.from({ length: count }, () => { + return kbnClient + .request({ + method: 'POST', + path: '/api/exception_lists/items', + body: generateTrustedAppEntry(), + }) + .then((item) => (item as unknown) as ExceptionListItemSchema); + }) + ); +}; + +interface GenerateTrustedAppEntryOptions { + os?: 'windows' | 'macos' | 'linux'; + name?: string; +} + +const generateTrustedAppEntry: (options?: GenerateTrustedAppEntryOptions) => object = ({ + os = 'windows', + name = `Sample Endpoint Trusted App Entry ${Date.now()}`, +} = {}) => { + return { + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + item_id: `generator_endpoint_trusted_apps_${generateUUID()}`, + _tags: ['endpoint', `os:${os}`], + tags: ['user added string for a tag', 'malware'], + type: 'simple', + description: 'This is a sample agnostic endpoint trusted app entry', + name, + namespace_type: 'agnostic', + entries: [ + { + field: 'actingProcess.file.signer', + operator: 'included', + type: 'match', + value: 'Elastic, N.V.', + }, + { + field: 'actingProcess.file.path', + operator: 'included', + type: 'match', + value: '/one/two/three', + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 0ec0db9f32776..6a8d56ff41a04 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -12,10 +12,12 @@ import { import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; import { getPackagePolicyCreateCallback } from './ingest_integration'; import { ManifestManager } from './services/artifacts'; +import { ExceptionListClient } from '../../../lists/server'; export type EndpointAppContextServiceStartContract = Partial< Pick > & { + exceptionsListService: ExceptionListClient; logger: Logger; manifestManager?: ManifestManager; registerIngestCallback?: IngestManagerStartContract['registerExternalCallback']; @@ -30,9 +32,11 @@ export class EndpointAppContextService { private agentService: AgentService | undefined; private manifestManager: ManifestManager | undefined; private savedObjectsStart: SavedObjectsServiceStart | undefined; + private exceptionsListService: ExceptionListClient | undefined; public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; + this.exceptionsListService = dependencies.exceptionsListService; this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; @@ -50,6 +54,13 @@ export class EndpointAppContextService { return this.agentService; } + public getExceptionsList() { + if (!this.exceptionsListService) { + throw new Error('exceptionsListService not set'); + } + return this.exceptionsListService; + } + public getManifestManager(): ManifestManager | undefined { return this.manifestManager; } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index b5f35a198fa9e..03754c7be7a5d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -21,6 +21,7 @@ import { import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager'; import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { EndpointAppContext } from './types'; +import { listMock } from '../../../lists/server/mocks'; /** * Creates a mocked EndpointAppContext. @@ -58,6 +59,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< > => { return { agentService: createMockAgentService(), + exceptionsListService: listMock.getExceptionListClient(), logger: loggingSystemMock.create().get('mock_endpoint_app_context'), savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), 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 new file mode 100644 index 0000000000000..6c29a2244c203 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -0,0 +1,49 @@ +/* + * 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 { RequestHandler } from 'kibana/server'; +import { + GetTrustedAppsListRequest, + GetTrustedListAppsResponse, +} from '../../../../common/endpoint/types'; +import { EndpointAppContext } from '../../types'; +import { exceptionItemToTrustedAppItem } from './utils'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; + +export const getTrustedAppsListRouteHandler = ( + endpointAppContext: EndpointAppContext +): RequestHandler => { + const logger = endpointAppContext.logFactory.get('trusted_apps'); + + return async (context, req, res) => { + const exceptionsListService = endpointAppContext.service.getExceptionsList(); + const { page, per_page: perPage } = req.query; + + try { + // 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', + }); + 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 }); + } + }; +}; 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 new file mode 100644 index 0000000000000..178aa06eee877 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { 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 { EndpointAppContext } from '../../types'; + +export const registerTrustedAppsRoutes = ( + router: IRouter, + endpointAppContext: EndpointAppContext +) => { + // GET list + router.get( + { + path: TRUSTED_APPS_LIST_API, + validate: GetTrustedAppsRequestSchema, + options: { authRequired: true }, + }, + getTrustedAppsListRouteHandler(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 new file mode 100644 index 0000000000000..1d4a7919b89f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -0,0 +1,108 @@ +/* + * 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, 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 { xpackMocks } from '../../../../../../mocks'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; +import { EndpointAppContext } from '../../types'; +import { ExceptionListClient } from '../../../../../lists/server'; + +describe('when invoking endpoint trusted apps route handlers', () => { + let routerMock: jest.Mocked; + let endpointAppContextService: EndpointAppContextService; + let context: ReturnType; + let response: ReturnType; + let exceptionsListClient: jest.Mocked; + let endpointAppContext: EndpointAppContext; + + beforeEach(() => { + routerMock = httpServiceMock.createRouter(); + endpointAppContextService = new EndpointAppContextService(); + const startContract = createMockEndpointAppContextServiceStartContract(); + exceptionsListClient = startContract.exceptionsListService as jest.Mocked; + endpointAppContextService.start(startContract); + endpointAppContext = { + ...createMockEndpointAppContext(), + service: endpointAppContextService, + }; + registerTrustedAppsRoutes(routerMock, endpointAppContext); + + // For use in individual API calls + context = xpackMocks.createRequestHandlerContext(); + 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 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 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(); + }); + }); +}); 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 new file mode 100644 index 0000000000000..2b417a4c6a8e1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts @@ -0,0 +1,42 @@ +/* + * 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/shared_exports'; +import { TrustedApp } from '../../../../common/endpoint/types'; + +/** + * 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, _tags, id } = exceptionListItem; + const os = osFromTagsList(_tags); + return { + entries, + description, + created_at, + created_by, + name, + os, + id, + } as TrustedApp; +}; + +/** + * Retrieves the OS entry from a list of tags (property returned with ExcptionListItem). + * For Trusted Apps each entry must have at MOST 1 OS. + * */ +const osFromTagsList = (tags: string[]): TrustedApp['os'] | 'unknown' => { + for (const tag of tags) { + if (tag.startsWith('os:')) { + return tag.substr(3) as TrustedApp['os']; + } + } + return 'unknown'; +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 1f08210aa41f3..38120bf42fbba 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -59,6 +59,7 @@ import { EndpointAppContext } from './endpoint/types'; import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts'; import { initUsageCollectors } from './usage'; import { AppRequestContext } from './types'; +import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; export interface SetupPlugins { @@ -166,6 +167,7 @@ export class Plugin implements IPlugin