[Fleet] backfill agentless package policies with supports_agentless field (#204410)

## Summary

Closes https://github.com/elastic/kibana/issues/203821

Added a function to Fleet setup to query package policies that are
missing `supports_agentless` field and backfilling them. Only doing this
for `cloud_security_posture` package, to skip other non-related packages
like `system`.

To verify:
- follow the steps in the description here to create an agentless agent
policy with cspm integration:
https://github.com/elastic/kibana/pull/199567
- manually update the package policy to simulate
`supports_agentless:false`
- trigger Fleet setup
- verify that the cspm package policy has `supports_agentless:true`

```
PUT kbn:/api/fleet/package_policies/<policy_id>
{
   "supports_agentless": false
}

POST kbn:/api/fleet/setup

GET kbn:/api/fleet/package_policies/<policy_id>
```

Logs:
```
[2024-12-16T15:42:11.027+01:00][DEBUG][plugins.fleet] Backfilling package policy supports_agentless field
[2024-12-16T15:42:11.034+01:00][DEBUG][plugins.fleet] Backfilling supports_agentless on package policies: 6a06d167-e02e-4057-9d71-e1f7e5dd2847
[2024-12-16T15:42:11.035+01:00][DEBUG][plugins.fleet] Starting update of package policy 6a06d167-e02e-4057-9d71-e1f7e5dd2847
[2024-12-16T15:42:13.213+01:00][DEBUG][plugins.fleet] Deploying policies: 0ed942d5-6c01-484f-a1c5-6c7fff92b020:12
[2024-12-16T15:42:13.610+01:00][DEBUG][plugins.fleet] Agent policy 0ed942d5-6c01-484f-a1c5-6c7fff92b020 update completed, revision: 12
[2024-12-16T15:42:13.610+01:00][DEBUG][plugins.fleet] Package policy 6a06d167-e02e-4057-9d71-e1f7e5dd2847 update completed
```

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Julia Bardi 2024-12-17 12:31:08 +01:00 committed by GitHub
parent 513e025f89
commit a229d7ab1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 170 additions and 0 deletions

View file

@ -0,0 +1,66 @@
/*
* 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 { backfillPackagePolicySupportsAgentless } from './backfill_agentless';
import { packagePolicyService } from './package_policy';
jest.mock('.', () => ({
appContextService: {
getLogger: () => ({
debug: jest.fn(),
}),
getInternalUserSOClientForSpaceId: jest.fn(),
getInternalUserSOClientWithoutSpaceExtension: () => ({
find: jest.fn().mockImplementation((options) => {
if (options.type === 'ingest-agent-policies') {
return {
saved_objects: [{ id: 'agent_policy_1' }, { id: 'agent_policy_2' }],
};
} else {
return {
saved_objects: [
{
id: 'package_policy_1',
attributes: {
inputs: [],
policy_ids: ['agent_policy_1'],
supports_agentless: false,
},
},
],
};
}
}),
}),
},
}));
jest.mock('./package_policy', () => ({
packagePolicyService: {
update: jest.fn(),
},
getPackagePolicySavedObjectType: jest.fn().mockResolvedValue('ingest-package-policies'),
}));
describe('backfill agentless package policies', () => {
it('should backfill package policies missing supports_agentless', async () => {
await backfillPackagePolicySupportsAgentless(undefined as any);
expect(packagePolicyService.update).toHaveBeenCalledWith(
undefined,
undefined,
'package_policy_1',
{
enabled: undefined,
inputs: [],
name: undefined,
policy_ids: ['agent_policy_1'],
supports_agentless: true,
}
);
});
});

View file

@ -0,0 +1,99 @@
/*
* 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 type { ElasticsearchClient } from '@kbn/core/server';
import pMap from 'p-map';
import { MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, SO_SEARCH_LIMIT } from '../constants';
import type { AgentPolicySOAttributes, PackagePolicy, PackagePolicySOAttributes } from '../types';
import { getAgentPolicySavedObjectType } from './agent_policy';
import { appContextService } from '.';
import { getPackagePolicySavedObjectType, packagePolicyService } from './package_policy';
import { mapPackagePolicySavedObjectToPackagePolicy } from './package_policies';
export async function backfillPackagePolicySupportsAgentless(esClient: ElasticsearchClient) {
const apSavedObjectType = await getAgentPolicySavedObjectType();
const internalSoClientWithoutSpaceExtension =
appContextService.getInternalUserSOClientWithoutSpaceExtension();
const findRes = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({
type: apSavedObjectType,
page: 1,
perPage: SO_SEARCH_LIMIT,
filter: `${apSavedObjectType}.attributes.supports_agentless:true`,
fields: [`id`],
namespaces: ['*'],
});
const agentPolicyIds = findRes.saved_objects.map((so) => so.id);
if (agentPolicyIds.length === 0) {
return;
}
const savedObjectType = await getPackagePolicySavedObjectType();
const packagePoliciesToUpdate = (
await appContextService
.getInternalUserSOClientWithoutSpaceExtension()
.find<PackagePolicySOAttributes>({
type: savedObjectType,
fields: [
'name',
'policy_ids',
'supports_agentless',
'enabled',
'policy_ids',
'inputs',
'package',
],
filter: `${savedObjectType}.attributes.package.name:cloud_security_posture AND (NOT ${savedObjectType}.attributes.supports_agentless:true) AND ${savedObjectType}.attributes.policy_ids:(${agentPolicyIds.join(
' OR '
)})`,
perPage: SO_SEARCH_LIMIT,
namespaces: ['*'],
})
).saved_objects.map((so) => mapPackagePolicySavedObjectToPackagePolicy(so, so.namespaces));
appContextService
.getLogger()
.debug(
`Backfilling supports_agentless on package policies: ${packagePoliciesToUpdate.map(
(policy) => policy.id
)}`
);
if (packagePoliciesToUpdate.length > 0) {
const getPackagePolicyUpdate = (packagePolicy: PackagePolicy) => ({
name: packagePolicy.name,
enabled: packagePolicy.enabled,
policy_ids: packagePolicy.policy_ids,
inputs: packagePolicy.inputs,
supports_agentless: true,
});
await pMap(
packagePoliciesToUpdate,
(packagePolicy) => {
const soClient = appContextService.getInternalUserSOClientForSpaceId(
packagePolicy.spaceIds?.[0]
);
return packagePolicyService.update(
soClient,
esClient,
packagePolicy.id,
getPackagePolicyUpdate(packagePolicy)
);
},
{
concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS,
}
);
}
}

View file

@ -33,6 +33,7 @@ jest.mock('./epm/elasticsearch/template/install', () => {
...jest.requireActual('./epm/elasticsearch/template/install'),
};
});
jest.mock('./backfill_agentless');
const mockedMethodThrowsError = (mockFn: jest.Mock) =>
mockFn.mockImplementation(() => {

View file

@ -62,6 +62,7 @@ import {
ensureDeleteUnenrolledAgentsSetting,
getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig,
} from './preconfiguration/delete_unenrolled_agent_setting';
import { backfillPackagePolicySupportsAgentless } from './backfill_agentless';
export interface SetupStatus {
isInitialized: boolean;
@ -305,6 +306,9 @@ async function createSetupSideEffects(
await ensureAgentPoliciesFleetServerKeysAndPolicies({ soClient, esClient, logger });
stepSpan?.end();
logger.debug('Backfilling package policy supports_agentless field');
await backfillPackagePolicySupportsAgentless(esClient);
const nonFatalErrors = [
...preconfiguredPackagesNonFatalErrors,
...(messageSigningServiceNonFatalError ? [messageSigningServiceNonFatalError] : []),