From d1701ca62d2df2090cac076d025a8fdf32e3b85d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Sat, 7 Dec 2024 08:14:37 +1100 Subject: [PATCH 1/3] [8.x] [Cloud Security] Refactoring the limit error message for agentless agent (#203257) (#203330) # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] Refactoring the limit error message for agentless agent (#203257)](https://github.com/elastic/kibana/pull/203257) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: seanrathier --- .../server/services/agents/agentless_agent.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) 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 6d1945fced809..3cd885beba455 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -37,6 +37,15 @@ import { listFleetServerHosts } from '../fleet_server_host'; import type { AgentlessConfig } from '../utils/agentless'; import { prependAgentlessApiBasePathToEndpoint, isAgentlessEnabled } from '../utils/agentless'; +interface AgentlessAgentErrorHandlingMessages { + [key: string]: { + [key: string]: { + log: string; + message: string; + }; + }; +} + class AgentlessAgentService { public async createAgentlessAgent( esClient: ElasticsearchClient, @@ -326,14 +335,12 @@ class AgentlessAgentService { throw this.getAgentlessAgentError(action, error.message, traceId); } - const ERROR_HANDLING_MESSAGES = this.getErrorHandlingMessages(agentlessPolicyId); + const ERROR_HANDLING_MESSAGES: AgentlessAgentErrorHandlingMessages = + this.getErrorHandlingMessages(agentlessPolicyId); if (error.response) { if (error.response.status in ERROR_HANDLING_MESSAGES) { - const handledResponseErrorMessage = - ERROR_HANDLING_MESSAGES[error.response.status as keyof typeof ERROR_HANDLING_MESSAGES][ - action - ]; + const handledResponseErrorMessage = ERROR_HANDLING_MESSAGES[error.response.status][action]; this.handleResponseError( action, error.response, @@ -426,7 +433,7 @@ class AgentlessAgentService { : new AgentlessAgentDeleteError(this.withRequestIdMessage(userMessage, traceId)); } - private getErrorHandlingMessages(agentlessPolicyId: string) { + private getErrorHandlingMessages(agentlessPolicyId: string): AgentlessAgentErrorHandlingMessages { return { 400: { create: { @@ -483,13 +490,7 @@ class AgentlessAgentService { create: { log: '[Agentless API] Creating the agentless agent failed with a status 429 for agentless policy, agentless agent limit has been reached for this deployment or project.', message: - 'the Agentless API could not create the agentless agent, you have reached the limit of agentless agents provisioned for this deployment or project. Consider removing some agentless agents and try again or use agent-based agents for this integration.', - }, - // this is likely to happen when deleting agentless agents, but covering it in case - delete: { - log: '[Agentless API] Deleting the agentless deployment failed with a status 429 for agentless policy, agentless agent limit has been reached for this deployment or project.', - message: - 'the Agentless API could not delete the agentless deployment, you have reached the limit of agentless agents provisioned for this deployment or project. Consider removing some agentless agents and try again or use agent-based agents for this integration.', + 'you have reached the limit for agentless provisioning. Please remove some or switch to agent-based integration.', }, }, 500: { From e8dc6995e69b49d2986c7037bf97c10b05a209e8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Sat, 7 Dec 2024 08:15:51 +1100 Subject: [PATCH 2/3] [8.x] [Cloud Security] Added 'x-elastic-internal-origin' to agentless axios request headers for Kibana 9.0 (#203188) (#203332) # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] Added 'x-elastic-internal-origin' to agentless axios request headers for Kibana 9.0 (#203188)](https://github.com/elastic/kibana/pull/203188) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: seanrathier --- .../services/agents/agentless_agent.test.ts | 1353 +++++++++-------- .../server/services/agents/agentless_agent.ts | 40 +- 2 files changed, 730 insertions(+), 663 deletions(-) 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 b278cda4fc278..042f9dce7f772 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 @@ -88,649 +88,6 @@ describe('Agentless Agent service', () => { jest.resetAllMocks(); }); - it('should throw AgentlessAgentConfigError if agentless policy does not support_agentless', async () => { - const soClient = getAgentPolicyCreateMock(); - // ignore unrelated unique name constraint - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - jest.spyOn(appContextService, 'getConfig').mockReturnValue({ - agentless: { - enabled: true, - api: { - url: 'http://api.agentless.com/api/v1/ess', - tls: { - certificate: '/path/to/cert', - key: '/path/to/key', - }, - }, - }, - } as any); - - await expect( - agentlessAgentService.createAgentlessAgent(esClient, soClient, { - id: 'mocked', - name: 'agentless agent policy', - namespace: 'default', - supports_agentless: false, - } as AgentPolicy) - ).rejects.toThrowError( - new AgentlessAgentConfigError( - 'Agentless agent policy does not have supports_agentless enabled' - ) - ); - }); - - it('should throw AgentlessAgentConfigError if cloud and serverless is not enabled', async () => { - const soClient = getAgentPolicyCreateMock(); - // ignore unrelated unique name constraint - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - jest - .spyOn(appContextService, 'getCloud') - .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: false } as any); - jest.spyOn(appContextService, 'getConfig').mockReturnValue({ - agentless: { - enabled: true, - api: { - url: 'http://api.agentless.com/api/v1/ess', - tls: { - certificate: '/path/to/cert', - key: '/path/to/key', - }, - }, - }, - } as any); - await expect( - agentlessAgentService.createAgentlessAgent(esClient, soClient, { - id: 'mocked', - name: 'agentless agent policy', - namespace: 'default', - supports_agentless: true, - } as AgentPolicy) - ).rejects.toThrowError( - new AgentlessAgentConfigError( - 'Agentless agents are only supported in cloud deployment and serverless projects' - ) - ); - }); - - it('should throw AgentlessAgentConfigError if agentless configuration is not found', async () => { - const soClient = getAgentPolicyCreateMock(); - // ignore unrelated unique name constraint - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - jest.spyOn(appContextService, 'getConfig').mockReturnValue({} as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - - await expect( - agentlessAgentService.createAgentlessAgent(esClient, soClient, { - id: 'mocked', - name: 'agentless agent policy', - namespace: 'default', - supports_agentless: true, - } as AgentPolicy) - ).rejects.toThrowError( - new AgentlessAgentConfigError('missing Agentless API configuration in Kibana') - ); - }); - - it('should throw AgentlessAgentConfigError if fleet hosts are not found', async () => { - const soClient = getAgentPolicyCreateMock(); - // ignore unrelated unique name constraint - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - jest.spyOn(appContextService, 'getConfig').mockReturnValue({ - agentless: { - enabled: true, - api: { - url: 'http://api.agentless.com/api/v1/ess', - tls: { - certificate: '/path/to/cert', - key: '/path/to/key', - }, - }, - }, - } as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedListFleetServerHosts.mockResolvedValue({ items: [] } as any); - mockedListEnrollmentApiKeys.mockResolvedValue({ - items: [ - { - id: 'mocked', - policy_id: 'mocked', - api_key: 'mocked', - }, - ], - } as any); - - await expect( - agentlessAgentService.createAgentlessAgent(esClient, soClient, { - id: 'mocked', - name: 'agentless agent policy', - namespace: 'default', - supports_agentless: true, - } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentConfigError('missing default Fleet server host')); - }); - - it('should throw AgentlessAgentConfigError if enrollment tokens are not found', async () => { - const soClient = getAgentPolicyCreateMock(); - // ignore unrelated unique name constraint - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - jest.spyOn(appContextService, 'getConfig').mockReturnValue({ - agentless: { - enabled: true, - api: { - url: 'http://api.agentless.com/api/v1/ess', - tls: { - certificate: '/path/to/cert', - key: '/path/to/key', - }, - }, - }, - } as any); - jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - mockedListFleetServerHosts.mockResolvedValue({ - items: [ - { - id: 'mocked', - host: 'http://fleetserver:8220', - active: true, - is_default: true, - }, - ], - } as any); - mockedListEnrollmentApiKeys.mockResolvedValue({ - items: [], - } as any); - - await expect( - agentlessAgentService.createAgentlessAgent(esClient, soClient, { - id: 'mocked', - name: 'agentless agent policy', - namespace: 'default', - supports_agentless: true, - } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentConfigError('missing Fleet enrollment token')); - }); - - it('should throw an error and log and error when the Agentless API returns a status not handled and not in the 2xx series', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 999, - data: { - message: 'This is a fake error status that is never to be handled handled', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toHaveBeenCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 500', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 500, - data: { - message: 'Internal Server Error', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toHaveBeenCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 429', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 429, - data: { - message: 'Limit exceeded', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toHaveBeenCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 408', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 408, - data: { - message: 'Request timed out', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toBeCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 404', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 404, - data: { - message: 'Not Found', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toBeCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 403', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 403, - data: { - message: 'Forbidden', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toBeCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 401', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 401, - data: { - message: 'Unauthorized', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toBeCalledTimes(1); - }); - - it('should throw an error and log and error when the Agentless API returns status 400', 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-policy-id', - api_key: 'mocked-api-key', - }, - ], - } as any); - // Force axios to throw an AxiosError to simulate an error response - (axios as jest.MockedFunction).mockRejectedValueOnce({ - response: { - status: 400, - data: { - message: 'Bad Request', - }, - }, - } as 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 the error is logged - expect(mockedLogger.error).toBeCalledTimes(1); - }); - it('should create agentless agent for ESS', async () => { const returnValue = { id: 'mocked', @@ -1267,4 +624,714 @@ describe('Agentless Agent service', () => { expect.any(Object) ); }); + + it(`should have x-elastic-internal-origin in the headers when the request is internal`, async () => { + const returnValue = { + id: 'mocked', + regional_id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + 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); + + expect(axios).toHaveBeenCalledTimes(1); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + 'x-elastic-internal-origin': 'Kibana', + }), + }) + ); + }); + + describe('error handling', () => { + it('should throw AgentlessAgentConfigError if agentless policy does not support_agentless', async () => { + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com/api/v1/ess', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + }, + }, + }, + } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: false, + } as AgentPolicy) + ).rejects.toThrowError( + new AgentlessAgentConfigError( + 'Agentless agent policy does not have supports_agentless enabled' + ) + ); + }); + + it('should throw AgentlessAgentConfigError if cloud and serverless is not enabled', async () => { + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: false } as any); + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com/api/v1/ess', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + }, + }, + }, + } as any); + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError( + new AgentlessAgentConfigError( + 'Agentless agents are only supported in cloud deployment and serverless projects' + ) + ); + }); + + it('should throw AgentlessAgentConfigError if agentless configuration is not found', async () => { + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({} as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError( + new AgentlessAgentConfigError('missing Agentless API configuration in Kibana') + ); + }); + + it('should throw AgentlessAgentConfigError if fleet hosts are not found', async () => { + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com/api/v1/ess', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ items: [] } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked', + policy_id: 'mocked', + api_key: 'mocked', + }, + ], + } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(new AgentlessAgentConfigError('missing default Fleet server host')); + }); + + it('should throw AgentlessAgentConfigError if enrollment tokens are not found', async () => { + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com/api/v1/ess', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [], + } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(new AgentlessAgentConfigError('missing Fleet enrollment token')); + }); + + it('should throw an error and log and error when the Agentless API returns a status not handled and not in the 2xx series', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 999, + data: { + message: 'This is a fake error status that is never to be handled handled', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 500', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 500, + data: { + message: 'Internal Server Error', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 429', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 429, + data: { + message: 'Limit exceeded', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 408', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 408, + data: { + message: 'Request timed out', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 404', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 404, + data: { + message: 'Not Found', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 403', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 403, + data: { + message: 'Forbidden', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 401', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 401, + data: { + message: 'Unauthorized', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + + it('should throw an error and log and error when the Agentless API returns status 400', 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-policy-id', + api_key: 'mocked-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + (axios as jest.MockedFunction).mockRejectedValueOnce({ + response: { + status: 400, + data: { + message: 'Bad Request', + }, + }, + } as 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 the error is logged + expect(mockedLogger.error).toBeCalledTimes(1); + }); + }); }); 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 3cd885beba455..3d6c8bba563ab 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -113,16 +113,7 @@ class AgentlessAgentService { labels, }, method: 'POST', - headers: { - 'Content-type': 'application/json', - 'X-Request-ID': traceId, - }, - httpsAgent: new https.Agent({ - rejectUnauthorized: tlsConfig.rejectUnauthorized, - cert: tlsConfig.certificate, - key: tlsConfig.key, - ca: tlsConfig.certificateAuthorities, - }), + ...this.getHeaders(tlsConfig, traceId), }; const cloudSetup = appContextService.getCloud(); @@ -157,6 +148,7 @@ class AgentlessAgentService { public async deleteAgentlessAgent(agentlessPolicyId: string) { const logger = appContextService.getLogger(); + const traceId = apm.currentTransaction?.traceparent; const agentlessConfig = appContextService.getConfig()?.agentless; const tlsConfig = this.createTlsConfig(agentlessConfig); const requestConfig = { @@ -165,17 +157,9 @@ class AgentlessAgentService { `/deployments/${agentlessPolicyId}` ), method: 'DELETE', - headers: { - 'Content-type': 'application/json', - }, - httpsAgent: new https.Agent({ - rejectUnauthorized: tlsConfig.rejectUnauthorized, - cert: tlsConfig.certificate, - key: tlsConfig.key, - ca: tlsConfig.certificateAuthorities, - }), + ...this.getHeaders(tlsConfig, traceId), }; - const traceId = apm.currentTransaction?.traceparent; + const errorMetadata: LogMeta = { trace: { id: traceId, @@ -220,6 +204,22 @@ class AgentlessAgentService { return response; } + private getHeaders(tlsConfig: SslConfig, traceId: string | undefined) { + return { + headers: { + 'Content-type': 'application/json', + 'X-Request-ID': traceId, + 'x-elastic-internal-origin': 'Kibana', + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: tlsConfig.rejectUnauthorized, + cert: tlsConfig.certificate, + key: tlsConfig.key, + ca: tlsConfig.certificateAuthorities, + }), + }; + } + private getAgentlessTags(agentlessAgentPolicy: AgentPolicy) { if (!agentlessAgentPolicy.global_data_tags) { return undefined; From 68a94b7b49374be31de27e99f3af99c2498cd010 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 7 Dec 2024 03:33:53 +0000 Subject: [PATCH 3/3] skip flaky suite (#203346) --- .../fleet_api_integration/apis/agent_policy/agent_policy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index f601743e394ec..c5677d7761bca 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -158,7 +158,8 @@ export default function (providerContext: FtrProviderContext) { }); }); - describe('POST /api/fleet/agent_policies', () => { + // FLAKY: https://github.com/elastic/kibana/issues/203346 + describe.skip('POST /api/fleet/agent_policies', () => { let systemPkgVersion: string; before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');