From db5b8f6702ef94588d6a5283b719d28965d6cd14 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 29 May 2026 11:48:40 -0400 Subject: [PATCH 1/5] [Fleet] Allow to delete orphaned agentless policies --- .../agentless/agentless_policies.test.ts | 73 +++++++++++++++++++ .../services/agentless/agentless_policies.ts | 38 +++++++++- .../apis/agentless/agentless_policies.ts | 23 ++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts index 3bb9b114a5ef1..642d852c32b2c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts @@ -16,6 +16,7 @@ import { createAppContextStartContractMock, createPackagePolicyServiceMock } fro import { getPackageInfo } from '../epm/packages'; import { appContextService, cloudConnectorService } from '..'; import { agentPolicyService } from '../agent_policy'; +import { agentlessAgentService } from '../agents/agentless_agent'; import { AgentlessPoliciesServiceImpl } from './agentless_policies'; @@ -310,6 +311,78 @@ describe('AgentlessPoliciesService', () => { expect(jest.mocked(agentPolicyService.delete)).toHaveBeenCalledTimes(0); }); + + it('should clean up orphaned resources when agent policy is not found (404)', async () => { + const deleteAgentlessAgentSpy = jest + .spyOn(agentlessAgentService, 'deleteAgentlessAgent') + .mockResolvedValueOnce(undefined as any); + + jest.mocked(agentPolicyService.get).mockRejectedValueOnce({ + output: { statusCode: 404 }, + }); + + packagePolicyService.findAllForAgentPolicy.mockResolvedValueOnce([ + { id: 'orphaned-pp-1' }, + { id: 'orphaned-pp-2' }, + ] as any); + + packagePolicyService.delete.mockResolvedValueOnce([ + { id: 'orphaned-pp-1', success: true }, + { id: 'orphaned-pp-2', success: true }, + ] as any); + + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const logger = loggingSystemMock.createLogger(); + + const agentlessPoliciesService = new AgentlessPoliciesServiceImpl( + packagePolicyService, + soClient, + esClient, + logger + ); + + await agentlessPoliciesService.deleteAgentlessPolicy('orphaned-policy-id'); + + expect(jest.mocked(agentPolicyService.delete)).not.toHaveBeenCalled(); + expect(packagePolicyService.findAllForAgentPolicy).toHaveBeenCalledWith( + soClient, + 'orphaned-policy-id' + ); + expect(packagePolicyService.delete).toHaveBeenCalledWith( + soClient, + esClient, + ['orphaned-pp-1', 'orphaned-pp-2'], + expect.objectContaining({ force: true }) + ); + expect(deleteAgentlessAgentSpy).toHaveBeenCalledWith('orphaned-policy-id'); + + deleteAgentlessAgentSpy.mockRestore(); + }); + + it('should rethrow non-404 errors from agentPolicyService.get', async () => { + jest.mocked(agentPolicyService.get).mockRejectedValueOnce({ + output: { statusCode: 500 }, + message: 'Internal server error', + }); + + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const logger = loggingSystemMock.createLogger(); + + const agentlessPoliciesService = new AgentlessPoliciesServiceImpl( + packagePolicyService, + soClient, + esClient, + logger + ); + + await expect(() => + agentlessPoliciesService.deleteAgentlessPolicy('some-policy-id') + ).rejects.toEqual(expect.objectContaining({ output: { statusCode: 500 } })); + + expect(jest.mocked(agentPolicyService.delete)).not.toHaveBeenCalled(); + }); }); describe('createAgentlessPolicy with cloud connectors', () => { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts index 03688a120750a..de19b3c99b946 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts @@ -11,6 +11,7 @@ import { type Logger, type RequestHandlerContext, type SavedObjectsClientContract, + SavedObjectsErrorHelpers, } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; import { v4 as uuidv4 } from 'uuid'; @@ -274,7 +275,18 @@ export class AgentlessPoliciesServiceImpl implements AgentlessPoliciesService { ? appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined : undefined; - const agentPolicy = await agentPolicyService.get(this.soClient, policyId); + let agentPolicy; + try { + agentPolicy = await agentPolicyService.get(this.soClient, policyId); + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + this.logger.warn(`Agent policy ${policyId} not found, cleaning up orphaned resources`); + await this.deleteOrphanedAgentlessResources(policyId, user); + return; + } + throw e; + } + if (!agentPolicy?.supports_agentless) { throw new Error(`Policy ${policyId} is not an agentless policy`); } @@ -285,4 +297,28 @@ export class AgentlessPoliciesServiceImpl implements AgentlessPoliciesService { user, }); } + + private async deleteOrphanedAgentlessResources(policyId: string, user?: any) { + const packagePolicies = await this.packagePolicyService.findAllForAgentPolicy( + this.soClient, + policyId + ); + + if (packagePolicies.length > 0) { + await this.packagePolicyService.delete( + this.soClient, + this.esClient, + packagePolicies.map((pp) => pp.id), + { force: true, user: user ?? undefined } + ); + } + + try { + await agentlessAgentService.deleteAgentlessAgent(policyId); + } catch (e) { + this.logger.warn( + `Failed to delete agentless deployment for orphaned policy ${policyId}: ${e.message}` + ); + } + } } diff --git a/x-pack/platform/test/fleet_api_integration/apis/agentless/agentless_policies.ts b/x-pack/platform/test/fleet_api_integration/apis/agentless/agentless_policies.ts index 4a9780a1c9b3b..0ba7a8e0dde25 100644 --- a/x-pack/platform/test/fleet_api_integration/apis/agentless/agentless_policies.ts +++ b/x-pack/platform/test/fleet_api_integration/apis/agentless/agentless_policies.ts @@ -329,6 +329,29 @@ export default function (providerContext: FtrProviderContext) { expect(apiCalls[0].url).to.be(`/agentless-api/api/v1/ess/deployments/${policyId}`); expect(apiCalls[0].method).to.be('DELETE'); }); + + it('should allow to delete an orphaned agentless policy when agent policy is missing', async () => { + // Orphan the policy by directly deleting the agent policy SO + await es.delete({ + index: '.kibana_ingest', + id: `fleet-agent-policies:${policyId}`, + refresh: 'wait_for', + }); + + // Verify agent policy is gone + await expectToRejectWithNotFound(() => apiClient.getAgentPolicy(policyId)); + + // Delete via agentless API should succeed despite missing agent policy + await apiClient.deleteAgentlessPolicy(policyId); + + // Verify package policy is cleaned up + await expectToRejectWithNotFound(() => apiClient.getPackagePolicy(policyId)); + + // Verify agentless API DELETE was called to clean up the deployment + expect(apiCalls.length).to.be(1); + expect(apiCalls[0].url).to.be(`/agentless-api/api/v1/ess/deployments/${policyId}`); + expect(apiCalls[0].method).to.be('DELETE'); + }); }); describe('Update Agentless Policy', () => { From b2e93b66ba1a37d8fc5de928c297932da1c55f02 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 29 May 2026 13:25:37 -0400 Subject: [PATCH 2/5] fix tests --- .../server/services/agentless/agentless_policies.test.ts | 7 ++++--- .../fleet/server/services/agentless/agentless_policies.ts | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts index 642d852c32b2c..9cbf270c1888f 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts @@ -17,6 +17,7 @@ import { getPackageInfo } from '../epm/packages'; import { appContextService, cloudConnectorService } from '..'; import { agentPolicyService } from '../agent_policy'; import { agentlessAgentService } from '../agents/agentless_agent'; +import { AgentPolicyNotFoundError } from '../../errors'; import { AgentlessPoliciesServiceImpl } from './agentless_policies'; @@ -317,9 +318,9 @@ describe('AgentlessPoliciesService', () => { .spyOn(agentlessAgentService, 'deleteAgentlessAgent') .mockResolvedValueOnce(undefined as any); - jest.mocked(agentPolicyService.get).mockRejectedValueOnce({ - output: { statusCode: 404 }, - }); + jest + .mocked(agentPolicyService.get) + .mockRejectedValueOnce(new AgentPolicyNotFoundError('Agent policy not found')); packagePolicyService.findAllForAgentPolicy.mockResolvedValueOnce([ { id: 'orphaned-pp-1' }, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts index de19b3c99b946..ba8dd51b48625 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts @@ -29,6 +29,7 @@ import type { PackagePolicyClient } from '../package_policy_service'; import { agentPolicyService } from '../agent_policy'; import { getPackageInfo } from '../epm/packages'; import { appContextService, cloudConnectorService } from '..'; +import { FleetNotFoundError } from '../../errors'; import type { PackageInfo } from '../../types'; import { @@ -279,7 +280,7 @@ export class AgentlessPoliciesServiceImpl implements AgentlessPoliciesService { try { agentPolicy = await agentPolicyService.get(this.soClient, policyId); } catch (e) { - if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + if (e instanceof FleetNotFoundError || SavedObjectsErrorHelpers.isNotFoundError(e)) { this.logger.warn(`Agent policy ${policyId} not found, cleaning up orphaned resources`); await this.deleteOrphanedAgentlessResources(policyId, user); return; From e0f86ad9b7231edbd039b41f65e58abfc2754871 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 29 May 2026 13:45:57 -0400 Subject: [PATCH 3/5] fix tests --- .../server/services/agentless/agentless_policies.test.ts | 4 ++-- .../fleet/server/services/agentless/agentless_policies.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts index 9cbf270c1888f..d72897b123d23 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.test.ts @@ -11,13 +11,13 @@ import { savedObjectsClientMock, } from '@kbn/core/server/mocks'; import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { createAppContextStartContractMock, createPackagePolicyServiceMock } from '../../mocks'; import { getPackageInfo } from '../epm/packages'; import { appContextService, cloudConnectorService } from '..'; import { agentPolicyService } from '../agent_policy'; import { agentlessAgentService } from '../agents/agentless_agent'; -import { AgentPolicyNotFoundError } from '../../errors'; import { AgentlessPoliciesServiceImpl } from './agentless_policies'; @@ -320,7 +320,7 @@ describe('AgentlessPoliciesService', () => { jest .mocked(agentPolicyService.get) - .mockRejectedValueOnce(new AgentPolicyNotFoundError('Agent policy not found')); + .mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('test')); packagePolicyService.findAllForAgentPolicy.mockResolvedValueOnce([ { id: 'orphaned-pp-1' }, diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts index ba8dd51b48625..ab36937ff1f3c 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts @@ -280,7 +280,7 @@ export class AgentlessPoliciesServiceImpl implements AgentlessPoliciesService { try { agentPolicy = await agentPolicyService.get(this.soClient, policyId); } catch (e) { - if (e instanceof FleetNotFoundError || SavedObjectsErrorHelpers.isNotFoundError(e)) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { this.logger.warn(`Agent policy ${policyId} not found, cleaning up orphaned resources`); await this.deleteOrphanedAgentlessResources(policyId, user); return; @@ -310,7 +310,7 @@ export class AgentlessPoliciesServiceImpl implements AgentlessPoliciesService { this.soClient, this.esClient, packagePolicies.map((pp) => pp.id), - { force: true, user: user ?? undefined } + { force: true, user: user ?? undefined, skipUnassignFromAgentPolicies: true } ); } From ddae68fc138bb709864fa5d402fdc052cc1af55d Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 29 May 2026 13:54:52 -0400 Subject: [PATCH 4/5] fix types --- .../shared/fleet/server/services/agentless/agentless_policies.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts index ab36937ff1f3c..18355f6185bd2 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts @@ -29,7 +29,6 @@ import type { PackagePolicyClient } from '../package_policy_service'; import { agentPolicyService } from '../agent_policy'; import { getPackageInfo } from '../epm/packages'; import { appContextService, cloudConnectorService } from '..'; -import { FleetNotFoundError } from '../../errors'; import type { PackageInfo } from '../../types'; import { From 3c2b1773c6e1df2b27447f52ac921b6ff2a65547 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 29 May 2026 14:05:10 -0400 Subject: [PATCH 5/5] fix tests --- .../fleet/server/services/agentless/agentless_policies.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts index 18355f6185bd2..d2ab6f45464a0 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/agentless/agentless_policies.ts @@ -29,6 +29,7 @@ import type { PackagePolicyClient } from '../package_policy_service'; import { agentPolicyService } from '../agent_policy'; import { getPackageInfo } from '../epm/packages'; import { appContextService, cloudConnectorService } from '..'; +import { FleetNotFoundError } from '../../errors'; import type { PackageInfo } from '../../types'; import { @@ -279,7 +280,7 @@ export class AgentlessPoliciesServiceImpl implements AgentlessPoliciesService { try { agentPolicy = await agentPolicyService.get(this.soClient, policyId); } catch (e) { - if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + if (e instanceof FleetNotFoundError || SavedObjectsErrorHelpers.isNotFoundError(e)) { this.logger.warn(`Agent policy ${policyId} not found, cleaning up orphaned resources`); await this.deleteOrphanedAgentlessResources(policyId, user); return;