diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 3338c8c86f2f2..647a8b917d0c0 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -32,12 +32,12 @@ export interface FleetConfigType { }; agentless?: { enabled: boolean; - api: { - url: string; - tls: { - certificate: string; - key: string; - ca: string; + api?: { + url?: string; + tls?: { + certificate?: string; + key?: string; + ca?: string; }; }; }; diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index 24c344c15eccc..e55b883e80029 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -9,6 +9,7 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import type { Logger } from '@kbn/core/server'; +import type { AxiosError } from 'axios'; import axios from 'axios'; import { AgentlessAgentCreateError } from '../../errors'; @@ -20,7 +21,14 @@ import { listFleetServerHosts } from '../fleet_server_host'; import { agentlessAgentService } from './agentless_agent'; -jest.mock('axios', () => jest.fn()); +jest.mock('axios'); +// Add a mock implementation for `isAxiosError` to simulate that the error is an Axios error +(axios.isAxiosError as unknown as jest.Mock).mockImplementation( + (error: any): error is AxiosError => { + return error.isAxiosError === true; // Simulate that the error is an Axios error if it has `isAxiosError` property + } +); + jest.mock('../fleet_server_host'); jest.mock('../api_keys'); jest.mock('../output'); @@ -361,4 +369,191 @@ describe('Agentless Agent service', () => { }) ); }); + + it('should redact sensitive information from debug logs', async () => { + const returnValue = { + id: 'mocked', + regional_id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest + .spyOn(appContextService, 'getKibanaVersion') + .mockReturnValue('mocked-kibana-version-infinite'); + + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + + await agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy); + + // Assert that sensitive information is redacted + expect(mockedLogger.debug).toHaveBeenCalledWith( + expect.stringContaining('fleet_token: [REDACTED]') + ); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('cert: [REDACTED]')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('key: [REDACTED]')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('ca: [REDACTED]')); + }); + + it('should log "undefined" on debug logs when tls configuration is missing', async () => { + const returnValue = { + id: 'mocked', + regional_id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest + .spyOn(appContextService, 'getKibanaVersion') + .mockReturnValue('mocked-kibana-version-infinite'); + + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that tls configuration is missing + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('cert: undefined')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('key: undefined')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('ca: undefined')); + }); + + it('should redact sensitive information from error logs', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + const axiosError = new Error('Test Error') as AxiosError; + axiosError.isAxiosError = true; // Mark it as an AxiosError + (axios as jest.MockedFunction).mockRejectedValueOnce(axiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that sensitive information is redacted + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining(`\"fleet_token\":\"[REDACTED]\"`), + expect.any(Object) + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 7f2c0dfaf1190..c98a5b63e0356 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -7,12 +7,14 @@ import https from 'https'; -import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ElasticsearchClient, LogMeta, SavedObjectsClientContract } from '@kbn/core/server'; import { SslConfig, sslSchema } from '@kbn/server-http-tools'; import type { AxiosError, AxiosRequestConfig } from 'axios'; import axios from 'axios'; +import apm from 'elastic-apm-node'; + import { SO_SEARCH_LIMIT } from '../../constants'; import type { AgentPolicy } from '../../types'; import type { AgentlessApiResponse } from '../../../common/types'; @@ -30,12 +32,22 @@ class AgentlessAgentService { soClient: SavedObjectsClientContract, agentlessAgentPolicy: AgentPolicy ) { + const traceId = apm.currentTransaction?.traceparent; + const withRequestIdMessage = (message: string) => `${message} [Request Id: ${traceId}]`; + + const errorMetadata: LogMeta = { + trace: { + id: traceId, + }, + }; + const logger = appContextService.getLogger(); logger.debug(`Creating agentless agent ${agentlessAgentPolicy.id}`); if (!isAgentlessApiEnabled) { logger.error( - 'Creating agentless agent not supported in non-cloud or non-serverless environments' + 'Creating agentless agent not supported in non-cloud or non-serverless environments', + errorMetadata ); throw new AgentlessAgentCreateError('Agentless agent not supported'); } @@ -46,7 +58,7 @@ class AgentlessAgentService { const agentlessConfig = appContextService.getConfig()?.agentless; if (!agentlessConfig) { - logger.error('Missing agentless configuration'); + logger.error('Missing agentless configuration', errorMetadata); throw new AgentlessAgentCreateError('missing agentless configuration'); } @@ -57,17 +69,23 @@ class AgentlessAgentService { soClient ); - logger.debug(`Creating agentless agent with fleetUrl ${fleetUrl} and fleetToken ${fleetToken}`); + logger.debug( + `Creating agentless agent with fleet_url: ${fleetUrl} and fleet_token: [REDACTED]` + ); - logger.debug(`Creating agentless agent with TLS config with certificate: ${agentlessConfig.api.tls.certificate}, - and key: ${agentlessConfig.api.tls.key}`); + logger.debug( + `Creating agentless agent with TLS cert: ${ + agentlessConfig?.api?.tls?.certificate ? '[REDACTED]' : 'undefined' + } and TLS key: ${agentlessConfig?.api?.tls?.key ? '[REDACTED]' : 'undefined'} + and TLS ca: ${agentlessConfig?.api?.tls?.ca ? '[REDACTED]' : 'undefined'}` + ); const tlsConfig = new SslConfig( sslSchema.validate({ enabled: true, - certificate: agentlessConfig.api.tls.certificate, - key: agentlessConfig.api.tls.key, - certificateAuthorities: agentlessConfig.api.tls.ca, + certificate: agentlessConfig?.api?.tls?.certificate, + key: agentlessConfig?.api?.tls?.key, + certificateAuthorities: agentlessConfig?.api?.tls?.ca, }) ); @@ -81,6 +99,7 @@ class AgentlessAgentService { method: 'POST', headers: { 'Content-type': 'application/json', + 'X-Request-ID': traceId, }, httpsAgent: new https.Agent({ rejectUnauthorized: tlsConfig.rejectUnauthorized, @@ -95,30 +114,45 @@ class AgentlessAgentService { requestConfig.data.stack_version = appContextService.getKibanaVersion(); } - const requestConfigDebug = JSON.stringify({ + const requestConfigDebug = { ...requestConfig, + data: { + ...requestConfig.data, + fleet_token: '[REDACTED]', + }, httpsAgent: { ...requestConfig.httpsAgent, options: { ...requestConfig.httpsAgent.options, - cert: requestConfig.httpsAgent.options.cert ? 'REDACTED' : undefined, - key: requestConfig.httpsAgent.options.key ? 'REDACTED' : undefined, - ca: requestConfig.httpsAgent.options.ca ? 'REDACTED' : undefined, + cert: requestConfig.httpsAgent.options.cert ? '[REDACTED]' : undefined, + key: requestConfig.httpsAgent.options.key ? '[REDACTED]' : undefined, + ca: requestConfig.httpsAgent.options.ca ? '[REDACTED]' : undefined, }, }, - }); + }; + + const requestConfigDebugToString = JSON.stringify(requestConfigDebug); - logger.debug(`Creating agentless agent with request config ${requestConfigDebug}`); + logger.debug(`Creating agentless agent with request config ${requestConfigDebugToString}`); + + const errorMetadataWithRequestConfig: LogMeta = { + ...errorMetadata, + http: { + request: { + id: traceId, + body: requestConfigDebug.data, + }, + }, + }; const response = await axios(requestConfig).catch( (error: Error | AxiosError) => { if (!axios.isAxiosError(error)) { logger.error( - `Creating agentless failed with an error ${error} ${JSON.stringify( - requestConfigDebug - )}` + `Creating agentless failed with an error ${error} ${requestConfigDebugToString}`, + errorMetadataWithRequestConfig ); - throw new AgentlessAgentCreateError(error.message); + throw new AgentlessAgentCreateError(withRequestIdMessage(error.message)); } const errorLogCodeCause = `${error.code} ${this.convertCauseErrorsToString(error)}`; @@ -128,28 +162,38 @@ class AgentlessAgentService { logger.error( `Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify( error.response.status - )}} ${JSON.stringify(error.response.data)}} ${JSON.stringify(requestConfigDebug)}` + )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugToString}`, + { + ...errorMetadataWithRequestConfig, + http: { + ...errorMetadataWithRequestConfig.http, + response: { + status_code: error.response.status, + body: error.response.data, + }, + }, + } ); throw new AgentlessAgentCreateError( - `the Agentless API could not create the agentless agent` + withRequestIdMessage(`the Agentless API could not create the agentless agent`) ); } else if (error.request) { // The request was made but no response was received logger.error( - `Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${JSON.stringify( - requestConfigDebug - )}` + `Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugToString}`, + errorMetadataWithRequestConfig + ); + throw new AgentlessAgentCreateError( + withRequestIdMessage(`no response received from the Agentless API`) ); - throw new AgentlessAgentCreateError(`no response received from the Agentless API`); } else { // Something happened in setting up the request that triggered an Error logger.error( - `Creating agentless agent failed to be created ${errorLogCodeCause} ${JSON.stringify( - requestConfigDebug - )}` + `Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugToString}`, + errorMetadataWithRequestConfig ); throw new AgentlessAgentCreateError( - 'the Agentless API could not create the agentless agent' + withRequestIdMessage('the Agentless API could not create the agentless agent') ); } }