diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 6913fc52d8c62..de00f623b829b 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -118,7 +118,7 @@ interface AgentBase { export interface Agent extends AgentBase { id: string; access_api_key?: string; - status?: AgentStatus; + status?: string; packages: string[]; } diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 72a7f4e35ddf5..9280b7f0b2e0d 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -35,8 +35,12 @@ export const getAgentHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { + const agent = await AgentService.getAgentById(esClient, request.params.agentId); const body: GetOneAgentResponse = { - item: await AgentService.getAgentById(esClient, request.params.agentId), + item: { + ...agent, + status: AgentService.getAgentStatus(agent), + }, }; return response.ok({ body }); @@ -87,8 +91,12 @@ export const updateAgentHandler: RequestHandler< await AgentService.updateAgent(esClient, request.params.agentId, { user_provided_metadata: request.body.user_provided_metadata, }); + const agent = await AgentService.getAgentById(esClient, request.params.agentId); const body = { - item: await AgentService.getAgentById(esClient, request.params.agentId), + item: { + ...agent, + status: AgentService.getAgentStatus(agent), + }, }; return response.ok({ body }); @@ -124,7 +132,10 @@ export const getAgentsHandler: RequestHandler< : 0; const body: GetAgentsResponse = { - list: agents, + list: agents.map((agent) => ({ + ...agent, + status: AgentService.getAgentStatus(agent), + })), total, totalInactive, page, diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 2efcf9d08a85a..33f2a5cb120c8 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -199,8 +199,9 @@ export async function getAgentById(esClient: ElasticsearchClient, agentId: strin if (agentHit.body.found === false) { throw agentNotFoundError; } + const agent = searchHitToAgent(agentHit.body); - return searchHitToAgent(agentHit.body); + return agent; } catch (err) { if (isESClientError(err) && err.meta.statusCode === 404) { throw agentNotFoundError; diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 195b2567c24ae..2618aad38bfbf 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -9,7 +9,6 @@ import type { estypes } from '@elastic/elasticsearch'; import type { SearchHit } from '../../../../../../src/core/types/elasticsearch'; import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; -import { getAgentStatus } from '../../../common/services/agent_status'; type FleetServerAgentESResponse = | estypes.MgetHit @@ -18,7 +17,7 @@ type FleetServerAgentESResponse = export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent { // @ts-expect-error @elastic/elasticsearch MultiGetHit._source is optional - const agent: Agent = { + return { id: hit._id, ...hit._source, policy_revision: hit._source?.policy_revision_idx, @@ -26,9 +25,6 @@ export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent { status: undefined, packages: hit._source?.packages ?? [], }; - - agent.status = getAgentStatus(agent); - return agent; } export function agentSOAttributesToFleetServerAgentDoc( diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 26cca630f9581..a0338aced65ff 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -20,7 +20,8 @@ export async function getAgentStatusById( esClient: ElasticsearchClient, agentId: string ): Promise { - return (await getAgentById(esClient, agentId)).status!; + const agent = await getAgentById(esClient, agentId); + return AgentStatusKueryHelper.getAgentStatus(agent); } export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts index a52fc205813f7..1c9adc8f2f9c3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -105,7 +105,7 @@ export class BaseDataGenerator { return [7, ...this.randomNGenerator(20, 2)].map((x) => x.toString()).join('.'); } - protected randomChoice(choices: T[] | readonly T[]): T { + protected randomChoice(choices: T[]): T { return choices[this.randomN(choices.length)]; } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts index 79888d9a97187..58e7539b1f617 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts @@ -9,19 +9,7 @@ import { estypes } from '@elastic/elasticsearch'; import { DeepPartial } from 'utility-types'; import { merge } from 'lodash'; import { BaseDataGenerator } from './base_data_generator'; -import { Agent, AGENTS_INDEX, AgentStatus, FleetServerAgent } from '../../../../fleet/common'; - -const agentStatusList: readonly AgentStatus[] = [ - 'offline', - 'error', - 'online', - 'inactive', - 'warning', - 'enrolling', - 'unenrolling', - 'updating', - 'degraded', -]; +import { Agent, AGENTS_INDEX, FleetServerAgent } from '../../../../fleet/common'; export class FleetAgentGenerator extends BaseDataGenerator { /** @@ -52,7 +40,7 @@ export class FleetAgentGenerator extends BaseDataGenerator { id: hit._id, policy_revision: hit._source?.policy_revision_idx, access_api_key: undefined, - status: this.randomAgentStatus(), + status: undefined, packages: hit._source?.packages ?? [], }, overrides @@ -128,8 +116,4 @@ export class FleetAgentGenerator extends BaseDataGenerator { overrides ); } - - private randomAgentStatus() { - return this.randomChoice(agentStatusList); - } } diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts index a16f2719ba90b..adfdc978ef271 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { httpServerMock } from '../../../../../src/core/server/mocks'; import { EndpointAppContextService } from './endpoint_app_context_services'; describe('test endpoint app context services', () => { @@ -16,4 +17,10 @@ describe('test endpoint app context services', () => { const endpointAppContextService = new EndpointAppContextService(); expect(endpointAppContextService.getManifestManager()).toEqual(undefined); }); + it('should throw error on getScopedSavedObjectsClient if start is not called', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(() => + endpointAppContextService.getScopedSavedObjectsClient(httpServerMock.createKibanaRequest()) + ).toThrow(Error); + }); }); 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 1a2e1a7c4ee6f..a4d900c514190 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 @@ -5,7 +5,12 @@ * 2.0. */ -import { KibanaRequest, Logger } from 'src/core/server'; +import { + KibanaRequest, + Logger, + SavedObjectsServiceStart, + SavedObjectsClientContract, +} from 'src/core/server'; import { ExceptionListClient } from '../../../lists/server'; import { CasesClient, @@ -31,8 +36,6 @@ import { ExperimentalFeatures, parseExperimentalConfigValue, } from '../../common/experimental_features'; -import { EndpointMetadataService } from './services/metadata'; -import { EndpointAppContentServicesNotStartedError } from './errors'; export type EndpointAppContextServiceStartContract = Partial< Pick< @@ -41,13 +44,13 @@ export type EndpointAppContextServiceStartContract = Partial< > > & { logger: Logger; - endpointMetadataService: EndpointMetadataService; manifestManager?: ManifestManager; appClientFactory: AppClientFactory; security: SecurityPluginStart; alerting: AlertsPluginStartContract; config: ConfigType; registerIngestCallback?: FleetStartContract['registerExternalCallback']; + savedObjectsStart: SavedObjectsServiceStart; licenseService: LicenseService; exceptionListsClient: ExceptionListClient | undefined; cases: CasesPluginStartContract | undefined; @@ -62,11 +65,12 @@ export class EndpointAppContextService { private manifestManager: ManifestManager | undefined; private packagePolicyService: PackagePolicyServiceInterface | undefined; private agentPolicyService: AgentPolicyServiceInterface | undefined; + private savedObjectsStart: SavedObjectsServiceStart | undefined; private config: ConfigType | undefined; private license: LicenseService | undefined; public security: SecurityPluginStart | undefined; private cases: CasesPluginStartContract | undefined; - private endpointMetadataService: EndpointMetadataService | undefined; + private experimentalFeatures: ExperimentalFeatures | undefined; public start(dependencies: EndpointAppContextServiceStartContract) { @@ -74,11 +78,12 @@ export class EndpointAppContextService { this.packagePolicyService = dependencies.packagePolicyService; this.agentPolicyService = dependencies.agentPolicyService; this.manifestManager = dependencies.manifestManager; + this.savedObjectsStart = dependencies.savedObjectsStart; this.config = dependencies.config; this.license = dependencies.licenseService; this.security = dependencies.security; this.cases = dependencies.cases; - this.endpointMetadataService = dependencies.endpointMetadataService; + this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental); if (this.manifestManager && dependencies.registerIngestCallback) { @@ -111,13 +116,6 @@ export class EndpointAppContextService { return this.experimentalFeatures; } - public getEndpointMetadataService(): EndpointMetadataService { - if (!this.endpointMetadataService) { - throw new EndpointAppContentServicesNotStartedError(); - } - return this.endpointMetadataService; - } - public getAgentService(): AgentService | undefined { return this.agentService; } @@ -134,16 +132,23 @@ export class EndpointAppContextService { return this.manifestManager; } + public getScopedSavedObjectsClient(req: KibanaRequest): SavedObjectsClientContract { + if (!this.savedObjectsStart) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.savedObjectsStart.getScopedClient(req, { excludedWrappers: ['security'] }); + } + public getLicenseService(): LicenseService { if (!this.license) { - throw new EndpointAppContentServicesNotStartedError(); + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); } return this.license; } public async getCasesClient(req: KibanaRequest): Promise { if (!this.cases) { - throw new EndpointAppContentServicesNotStartedError(); + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); } return this.cases.getCasesClientWithRequest(req); } diff --git a/x-pack/plugins/security_solution/server/endpoint/errors.ts b/x-pack/plugins/security_solution/server/endpoint/errors.ts deleted file mode 100644 index 6bd664401b449..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/errors.ts +++ /dev/null @@ -1,24 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ - -export class EndpointError extends Error { - constructor(message: string, public readonly meta?: unknown) { - super(message); - // For debugging - capture name of subclasses - this.name = this.constructor.name; - } -} - -export class NotFoundError extends EndpointError {} - -export class EndpointAppContentServicesNotStartedError extends EndpointError { - constructor() { - super('EndpointAppContextService has not been started (EndpointAppContextService.start())'); - } -} diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index d442e4c8a7e5a..2af6a944985ae 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -37,7 +37,6 @@ import { parseExperimentalConfigValue } from '../../common/experimental_features // a restricted path. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createCasesClientMock } from '../../../cases/server/client/mocks'; -import { EndpointMetadataService } from './services/metadata'; /** * Creates a mocked EndpointAppContext. @@ -66,6 +65,7 @@ export const createMockEndpointAppContextService = ( getAgentService: jest.fn(), getAgentPolicyService: jest.fn(), getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()), + getScopedSavedObjectsClient: jest.fn(), } as unknown) as jest.Mocked; }; @@ -76,23 +76,14 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< const factory = new AppClientFactory(); const config = createMockConfig(); const casesClientMock = createCasesClientMock(); - const savedObjectsStart = savedObjectsServiceMock.createStartContract(); - const agentService = createMockAgentService(); - const agentPolicyService = createMockAgentPolicyService(); - const endpointMetadataService = new EndpointMetadataService( - savedObjectsStart, - agentService, - agentPolicyService - ); factory.setup({ getSpaceId: () => 'mockSpace', config }); return { - agentService, - agentPolicyService, - endpointMetadataService, + agentService: createMockAgentService(), packageService: createMockPackageService(), logger: loggingSystemMock.create().get('mock_endpoint_app_context'), + savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), appClientFactory: factory, security: securityMock.createStart(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index af8c4d347c773..2ceca170881e3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -9,9 +9,7 @@ import Boom from '@hapi/boom'; import { TypeOf } from '@kbn/config-schema'; import { - IKibanaResponse, IScopedClusterClient, - KibanaResponseFactory, Logger, RequestHandler, SavedObjectsClientContract, @@ -37,8 +35,6 @@ import { queryResponseToHostListResult, queryResponseToHostResult, } from './support/query_strategies'; -import { NotFoundError } from '../../errors'; -import { EndpointHostUnEnrolledError } from '../../services/metadata'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -62,33 +58,6 @@ export const getLogger = (endpointAppContext: EndpointAppContext): Logger => { return endpointAppContext.logFactory.get('metadata'); }; -const errorHandler = ( - logger: Logger, - res: KibanaResponseFactory, - error: E -): IKibanaResponse => { - if (error instanceof NotFoundError) { - return res.notFound({ body: error }); - } - - if (error instanceof EndpointHostUnEnrolledError) { - return res.badRequest({ body: error }); - } - - // legacy check for Boom errors. `ts-ignore` is for the errors around non-standard error properties - // @ts-ignore - const boomStatusCode = error.isBoom && error?.output?.statusCode; - if (boomStatusCode) { - return res.customError({ - statusCode: boomStatusCode, - body: error, - }); - } - - // Kibana CORE will take care of `500` errors when the handler `throw`'s, including logging the error - throw error; -}; - export const getMetadataListRequestHandler = function ( endpointAppContext: EndpointAppContext, logger: Logger @@ -153,17 +122,34 @@ export const getMetadataRequestHandler = function ( SecuritySolutionRequestHandlerContext > { return async (context, request, response) => { - const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService(); + const agentService = endpointAppContext.service.getAgentService(); + if (agentService === undefined) { + throw new Error('agentService not available'); + } + + const metadataRequestContext: MetadataRequestContext = { + esClient: context.core.elasticsearch.client, + endpointAppContextService: endpointAppContext.service, + logger, + requestHandlerContext: context, + savedObjectsClient: context.core.savedObjects.client, + }; try { - return response.ok({ - body: await endpointMetadataService.getEnrichedHostMetadata( - context.core.elasticsearch.client.asCurrentUser, - request.params.id - ), - }); - } catch (error) { - return errorHandler(logger, response, error); + const doc = await getHostData(metadataRequestContext, request?.params?.id); + if (doc) { + return response.ok({ body: doc }); + } + return response.notFound({ body: 'Endpoint Not Found' }); + } catch (err) { + logger.warn(JSON.stringify(err, null, 2)); + if (err.isBoom) { + return response.customError({ + statusCode: err.output.statusCode, + body: { message: err.message }, + }); + } + throw err; } }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 9e5af2ed2d2bc..1e56f79aa0b32 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -11,6 +11,7 @@ import { RouteConfig, SavedObjectsClientContract, } from 'kibana/server'; +import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server/'; import { elasticsearchServiceMock, httpServerMock, @@ -40,14 +41,12 @@ import { metadataTransformPrefix, } from '../../../../common/endpoint/constants'; import type { SecuritySolutionPluginRouter } from '../../../types'; -import { AgentNotFoundError, PackagePolicyServiceInterface } from '../../../../../fleet/server'; +import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { ClusterClientMock, ScopedClusterClientMock, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; -import { EndpointHostNotFoundError } from '../../services/metadata'; -import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -72,7 +71,6 @@ describe('test endpoint route', () => { page: 1, perPage: 1, }; - const agentGenerator = new FleetAgentGenerator('seed'); beforeEach(() => { mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -339,7 +337,7 @@ describe('test endpoint route', () => { }); expect(mockResponse.notFound).toBeCalled(); const message = mockResponse.notFound.mock.calls[0][0]?.body; - expect(message).toBeInstanceOf(EndpointHostNotFoundError); + expect(message).toEqual('Endpoint Not Found'); }); it('should return a single endpoint with status healthy', async () => { @@ -348,9 +346,10 @@ describe('test endpoint route', () => { params: { id: response.hits.hits[0]._id }, }); - mockAgentService.getAgent = jest - .fn() - .mockReturnValue(agentGenerator.generate({ status: 'online' })); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => Promise.resolve({ body: response }) ); @@ -383,9 +382,13 @@ describe('test endpoint route', () => { params: { id: response.hits.hits[0]._id }, }); - mockAgentService.getAgent = jest - .fn() - .mockRejectedValue(new AgentNotFoundError('not found')); + mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { + SavedObjectsErrorHelpers.createGenericNotFoundError(); + }); + + mockAgentService.getAgent = jest.fn().mockImplementation(() => { + SavedObjectsErrorHelpers.createGenericNotFoundError(); + }); (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => Promise.resolve({ body: response }) @@ -418,11 +421,10 @@ describe('test endpoint route', () => { params: { id: response.hits.hits[0]._id }, }); - mockAgentService.getAgent = jest.fn().mockReturnValue( - agentGenerator.generate({ - status: 'error', - }) - ); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => Promise.resolve({ body: response }) ); @@ -471,7 +473,7 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(mockResponse.badRequest).toBeCalled(); + expect(mockResponse.customError).toBeCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts index 3d03040dd2605..b366f4a997730 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts @@ -7,9 +7,7 @@ /* eslint-disable max-classes-per-file */ -import { NotFoundError } from '../../errors'; - -export class TrustedAppNotFoundError extends NotFoundError { +export class TrustedAppNotFoundError extends Error { constructor(id: string) { super(`Trusted Application (${id}) not found`); } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts index cd0d1ac11fd2e..ee6570c4866bd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts @@ -6,5 +6,5 @@ */ export * from './artifacts'; -export { getMetadataForEndpoints } from './metadata/metadata'; +export { getMetadataForEndpoints } from './metadata'; export * from './actions'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/metadata.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts similarity index 69% rename from x-pack/plugins/security_solution/server/endpoint/services/metadata/metadata.ts rename to x-pack/plugins/security_solution/server/endpoint/services/metadata.ts index 2700bd80ca073..1a5515d8122f1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/metadata.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts @@ -7,12 +7,10 @@ import { SearchRequest } from '@elastic/elasticsearch/api/types'; import { SearchResponse } from 'elasticsearch'; -import { HostMetadata } from '../../../../common/endpoint/types'; -import { SecuritySolutionRequestHandlerContext } from '../../../types'; -import { getESQueryHostMetadataByIDs } from '../../routes/metadata/query_builders'; -import { queryResponseToHostListResult } from '../../routes/metadata/support/query_strategies'; - -// FIXME: fold this function into the EndpointMetadaService +import { HostMetadata } from '../../../common/endpoint/types'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders'; +import { queryResponseToHostListResult } from '../routes/metadata/support/query_strategies'; export async function getMetadataForEndpoints( endpointIDs: string[], diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts deleted file mode 100644 index 6745759727f07..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ /dev/null @@ -1,226 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ElasticsearchClient, - Logger, - SavedObjectsClientContract, - SavedObjectsServiceStart, -} from 'kibana/server'; -import { HostInfo, HostMetadata } from '../../../../common/endpoint/types'; -import { Agent, AgentPolicy, PackagePolicy } from '../../../../../fleet/common'; -import { - AgentNotFoundError, - AgentPolicyServiceInterface, - AgentService, -} from '../../../../../fleet/server'; -import { - EndpointHostNotFoundError, - EndpointHostUnEnrolledError, - FleetAgentNotFoundError, - FleetAgentPolicyNotFoundError, -} from './errors'; -import { getESQueryHostMetadataByID } from '../../routes/metadata/query_builders'; -import { queryResponseToHostResult } from '../../routes/metadata/support/query_strategies'; -import { DEFAULT_ENDPOINT_HOST_STATUS, fleetAgentStatusToEndpointHostStatus } from '../../utils'; -import { EndpointError } from '../../errors'; -import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client'; - -type AgentPolicyWithPackagePolicies = Omit & { - package_policies: PackagePolicy[]; -}; - -// Will wrap the given Error with `EndpointError`, which will -// help getting a good picture of where in our code the error originated from. -const wrapErrorIfNeeded = (error: Error): EndpointError => - error instanceof EndpointError ? error : new EndpointError(error.message, error); - -// used as the callback to `Promise#catch()` to ensure errors -// (especially those from kibana/elasticsearch clients) are wrapped -const catchAndWrapError = (error: E) => Promise.reject(wrapErrorIfNeeded(error)); - -export class EndpointMetadataService { - /** - * For internal use only by the `this.DANGEROUS_INTERNAL_SO_CLIENT` - * @deprecated - */ - private __DANGEROUS_INTERNAL_SO_CLIENT: SavedObjectsClientContract | undefined; - - constructor( - private savedObjectsStart: SavedObjectsServiceStart, - private readonly agentService: AgentService, - private readonly agentPolicyService: AgentPolicyServiceInterface, - private readonly logger?: Logger - ) {} - - /** - * An INTERNAL Saved Object client that is effectively the system user and has all privileges and permissions and - * can access any saved object. Used primarly to retrieve fleet data for endpoint enrichment (so that users are - * not required to have superuser role) - * - * **IMPORTANT: SHOULD BE USED ONLY FOR READ-ONLY ACCESS AND WITH DISCRETION** - * - * @private - */ - public get DANGEROUS_INTERNAL_SO_CLIENT() { - // The INTERNAL SO client must be created during the first time its used. This is because creating it during - // instance initialization (in `constructor(){}`) causes the SO Client to be invalid (perhaps because this - // instantiation is happening during the plugin's the start phase) - if (!this.__DANGEROUS_INTERNAL_SO_CLIENT) { - this.__DANGEROUS_INTERNAL_SO_CLIENT = createInternalReadonlySoClient(this.savedObjectsStart); - } - - return this.__DANGEROUS_INTERNAL_SO_CLIENT; - } - - /** - * Retrieve a single endpoint host metadata. Note that the return endpoint document, if found, - * could be associated with a Fleet Agent that is no longer active. If wanting to ensure the - * endpoint is associated with an active Fleet Agent, then use `getEnrichedHostMetadata()` instead - * - * @param esClient Elasticsearch Client (usually scoped to the user's context) - * @param endpointId the endpoint id (from `agent.id`) - * - * @throws - */ - async getHostMetadata(esClient: ElasticsearchClient, endpointId: string): Promise { - const query = getESQueryHostMetadataByID(endpointId); - const queryResult = await esClient.search(query).catch(catchAndWrapError); - const endpointMetadata = queryResponseToHostResult(queryResult.body).result; - - if (endpointMetadata) { - return endpointMetadata; - } - - throw new EndpointHostNotFoundError(`Endpoint with id ${endpointId} not found`); - } - - /** - * Retrieve a single endpoint host metadata along with fleet information - * - * @param esClient Elasticsearch Client (usually scoped to the user's context) - * @param endpointId the endpoint id (from `agent.id`) - * - * @throws - */ - async getEnrichedHostMetadata( - esClient: ElasticsearchClient, - endpointId: string - ): Promise { - const endpointMetadata = await this.getHostMetadata(esClient, endpointId); - - let fleetAgentId = endpointMetadata.elastic.agent.id; - let fleetAgent: Agent | undefined; - - // Get Fleet agent - try { - if (!fleetAgentId) { - fleetAgentId = endpointMetadata.agent.id; - this.logger?.warn(`Missing elastic agent id, using host id instead ${fleetAgentId}`); - } - - fleetAgent = await this.getFleetAgent(esClient, fleetAgentId); - } catch (error) { - if (error instanceof FleetAgentNotFoundError) { - this.logger?.warn(`agent with id ${fleetAgentId} not found`); - } else { - throw error; - } - } - - // If the agent is not longer active, then that means that the Agent/Endpoint have been un-enrolled from the host - if (fleetAgent && !fleetAgent.active) { - throw new EndpointHostUnEnrolledError( - `Endpoint with id ${endpointId} (Fleet agent id ${fleetAgentId}) is unenrolled` - ); - } - - // ------------------------------------------------------------------------------ - // Any failures in enriching the Host form this point should NOT cause an error - // ------------------------------------------------------------------------------ - try { - let fleetAgentPolicy: AgentPolicyWithPackagePolicies | undefined; - let endpointPackagePolicy: PackagePolicy | undefined; - - // Get Agent Policy and Endpoint Package Policy - if (fleetAgent) { - try { - fleetAgentPolicy = await this.getFleetAgentPolicy(fleetAgent.policy_id!); - endpointPackagePolicy = fleetAgentPolicy.package_policies.find( - (policy) => policy.package?.name === 'endpoint' - ); - } catch (error) { - this.logger?.error(error); - } - } - - return { - metadata: endpointMetadata, - host_status: fleetAgent - ? fleetAgentStatusToEndpointHostStatus(fleetAgent.status!) - : DEFAULT_ENDPOINT_HOST_STATUS, - policy_info: { - agent: { - applied: { - revision: fleetAgent?.policy_revision ?? 0, - id: fleetAgent?.policy_id ?? '', - }, - configured: { - revision: fleetAgentPolicy?.revision ?? 0, - id: fleetAgentPolicy?.id ?? '', - }, - }, - endpoint: { - revision: endpointPackagePolicy?.revision ?? 0, - id: endpointPackagePolicy?.id ?? '', - }, - }, - }; - } catch (error) { - throw wrapErrorIfNeeded(error); - } - } - - /** - * Retrieve a single Fleet Agent data - * - * @param esClient Elasticsearch Client (usually scoped to the user's context) - * @param agentId The elastic agent id (`from `elastic.agent.id`) - */ - async getFleetAgent(esClient: ElasticsearchClient, agentId: string): Promise { - try { - return await this.agentService.getAgent(esClient, agentId); - } catch (error) { - if (error instanceof AgentNotFoundError) { - throw new FleetAgentNotFoundError(`agent with id ${agentId} not found`, error); - } - - throw new EndpointError(error.message, error); - } - } - - /** - * Retrieve a specific Fleet Agent Policy - * - * @param agentPolicyId - * - * @throws - */ - async getFleetAgentPolicy(agentPolicyId: string): Promise { - const agentPolicy = await this.agentPolicyService - .get(this.DANGEROUS_INTERNAL_SO_CLIENT, agentPolicyId, true) - .catch(catchAndWrapError); - - if (agentPolicy) { - return agentPolicy as AgentPolicyWithPackagePolicies; - } - - throw new FleetAgentPolicyNotFoundError( - `Fleet agent policy with id ${agentPolicyId} not found` - ); - } -} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/errors.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/errors.ts deleted file mode 100644 index f61ad79a4c92b..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/errors.ts +++ /dev/null @@ -1,18 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ - -import { EndpointError, NotFoundError } from '../../errors'; - -export class EndpointHostNotFoundError extends NotFoundError {} - -export class EndpointHostUnEnrolledError extends EndpointError {} - -export class FleetAgentNotFoundError extends NotFoundError {} - -export class FleetAgentPolicyNotFoundError extends NotFoundError {} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/index.ts deleted file mode 100644 index 625382253e4ba..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/index.ts +++ /dev/null @@ -1,9 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './endpoint_metadata_service'; -export * from './errors'; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.test.ts deleted file mode 100644 index eee6b57232bc1..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.test.ts +++ /dev/null @@ -1,56 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - savedObjectsServiceMock, - savedObjectsClientMock, -} from '../../../../../../src/core/server/mocks'; -import { SavedObjectsClientContract } from 'kibana/server'; -import { - createInternalReadonlySoClient, - InternalReadonlySoClientMethodNotAllowedError, -} from './create_internal_readonly_so_client'; - -describe('When using the `createInternalReadonlySoClient`', () => { - let realSoClientMock: ReturnType; - let readonlySoClient: ReturnType; - - beforeEach(() => { - const soStartContract = savedObjectsServiceMock.createStartContract(); - realSoClientMock = savedObjectsClientMock.create(); - soStartContract.getScopedClient.mockReturnValue(realSoClientMock); - readonlySoClient = createInternalReadonlySoClient(soStartContract); - }); - - it.each([ - 'get', - 'bulkGet', - 'checkConflicts', - 'collectMultiNamespaceReferences', - 'find', - 'resolve', - ])('should allow usage of %s() method', (methodName) => { - expect(() => readonlySoClient[methodName]).not.toThrow(); - }); - - it.each([ - 'bulkCreate', - 'bulkUpdate', - 'create', - 'createPointInTimeFinder', - 'delete', - 'removeReferencesTo', - 'openPointInTimeForType', - 'closePointInTime', - 'update', - 'updateObjectsSpaces', - ])('should throw if usage of %s() is attempted', (methodName) => { - expect(() => readonlySoClient[methodName]).toThrow( - InternalReadonlySoClientMethodNotAllowedError - ); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts deleted file mode 100644 index b925146ff3b6c..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts +++ /dev/null @@ -1,63 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaRequest, SavedObjectsClientContract, SavedObjectsServiceStart } from 'kibana/server'; -import { EndpointError } from '../errors'; - -type SavedObjectsClientContractKeys = keyof SavedObjectsClientContract; - -const RESTRICTED_METHODS: readonly SavedObjectsClientContractKeys[] = [ - 'bulkCreate', - 'bulkUpdate', - 'create', - 'createPointInTimeFinder', - 'delete', - 'removeReferencesTo', - 'openPointInTimeForType', - 'closePointInTime', - 'update', - 'updateObjectsSpaces', -]; - -export class InternalReadonlySoClientMethodNotAllowedError extends EndpointError {} - -/** - * Creates an internal (system user) Saved Objects client (permissions turned off) that can only perform READ - * operations. - */ -export const createInternalReadonlySoClient = ( - savedObjectsServiceStart: SavedObjectsServiceStart -): SavedObjectsClientContract => { - const fakeRequest = ({ - headers: {}, - getBasePath: () => '', - path: '/', - route: { settings: {} }, - url: { href: {} }, - raw: { req: { url: '/' } }, - } as unknown) as KibanaRequest; - - const internalSoClient = savedObjectsServiceStart.getScopedClient(fakeRequest, { - excludedWrappers: ['security'], - }); - - return new Proxy(internalSoClient, { - get( - target: SavedObjectsClientContract, - methodName: SavedObjectsClientContractKeys, - receiver: unknown - ): unknown { - if (RESTRICTED_METHODS.includes(methodName)) { - throw new InternalReadonlySoClientMethodNotAllowedError( - `Method [${methodName}] not allowed on internal readonly SO Client` - ); - } - - return Reflect.get(target, methodName, receiver); - }, - }) as SavedObjectsClientContract; -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts b/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts index 5570de1f63461..3c02222346a44 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/fleet_agent_status_to_endpoint_host_status.ts @@ -8,8 +8,6 @@ import { AgentStatus } from '../../../../fleet/common'; import { HostStatus } from '../../../common/endpoint/types'; -// For an understanding of how fleet agent status is calculated: -// @see `x-pack/plugins/fleet/common/services/agent_status.ts` const STATUS_MAPPING: ReadonlyMap = new Map([ ['online', HostStatus.HEALTHY], ['offline', HostStatus.OFFLINE], @@ -22,12 +20,10 @@ const STATUS_MAPPING: ReadonlyMap = new Map([ ['degraded', HostStatus.UNHEALTHY], ]); -export const DEFAULT_ENDPOINT_HOST_STATUS = HostStatus.UNHEALTHY; - /** * A Map of Fleet Agent Status to Endpoint Host Status. * Default status is `HostStatus.UNHEALTHY` */ export const fleetAgentStatusToEndpointHostStatus = (status: AgentStatus): HostStatus => { - return STATUS_MAPPING.get(status) || DEFAULT_ENDPOINT_HOST_STATUS; + return STATUS_MAPPING.get(status) || HostStatus.UNHEALTHY; }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 2c0be2ac93321..2726afdf31ee0 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -92,7 +92,6 @@ import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; import { getKibanaPrivilegesFeaturePrivileges } from './features'; -import { EndpointMetadataService } from './endpoint/services/metadata'; export interface SetupPlugins { alerting: AlertingSetup; @@ -396,12 +395,6 @@ export class Plugin implements IPlugin