[Security Solution] Move policy meta updates to policy update (#155462)

## Summary

Moving the new meta fields in Policy to update when the Policy update
callback is called.

These fields are used in telemetry. These fields are being moved from
the Policy watcher to avoid triggering a policy deploy on many Agents at
once on upgrade. Instead, these fields will be updated whenever the next
Endpoint policy update comes.

New Policies will have the telemetry fields already populated.

### 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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kevin Logan 2023-04-24 09:07:15 -04:00 committed by GitHub
parent 9eee24f7bf
commit b5c88d90ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 36 deletions

View file

@ -125,7 +125,8 @@ export class EndpointAppContextService {
logger,
licenseService,
featureUsageService,
endpointMetadataService
endpointMetadataService,
cloud
)
);

View file

@ -16,7 +16,6 @@ import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks';
import { PolicyWatcher } from './license_watch';
import type { ILicense } from '@kbn/licensing-plugin/common/types';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import { cloudMock } from '@kbn/cloud-plugin/server/mocks';
import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks';
@ -36,7 +35,6 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa
describe('Policy-Changing license watcher', () => {
const logger = loggingSystemMock.create().get('license_watch.test');
const cloudServiceMock = cloudMock.createSetup();
const soStartMock = savedObjectsServiceMock.createStartContract();
const esStartMock = elasticsearchServiceMock.createStart();
let packagePolicySvcMock: jest.Mocked<PackagePolicyClient>;
@ -53,13 +51,7 @@ describe('Policy-Changing license watcher', () => {
// mock a license-changing service to test reactivity
const licenseEmitter: Subject<ILicense> = new Subject();
const licenseService = new LicenseService();
const pw = new PolicyWatcher(
packagePolicySvcMock,
soStartMock,
esStartMock,
cloudServiceMock,
logger
);
const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger);
// swap out watch function, just to ensure it gets called when a license change happens
const mockWatch = jest.fn();
@ -104,13 +96,7 @@ describe('Policy-Changing license watcher', () => {
perPage: 100,
});
const pw = new PolicyWatcher(
packagePolicySvcMock,
soStartMock,
esStartMock,
cloudServiceMock,
logger
);
const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger);
await pw.watch(Gold); // just manually trigger with a given license
expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts
@ -137,13 +123,7 @@ describe('Policy-Changing license watcher', () => {
perPage: 100,
});
const pw = new PolicyWatcher(
packagePolicySvcMock,
soStartMock,
esStartMock,
cloudServiceMock,
logger
);
const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger);
// emulate a license change below paid tier
await pw.watch(Basic);

View file

@ -17,7 +17,6 @@ import type {
} from '@kbn/core/server';
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
import type { ILicense } from '@kbn/licensing-plugin/common/types';
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
@ -35,19 +34,16 @@ export class PolicyWatcher {
private policyService: PackagePolicyClient;
private subscription: Subscription | undefined;
private soStart: SavedObjectsServiceStart;
private cloud: CloudSetup;
constructor(
policyService: PackagePolicyClient,
soStart: SavedObjectsServiceStart,
esStart: ElasticsearchServiceStart,
cloud: CloudSetup,
logger: Logger
) {
this.policyService = policyService;
this.esClient = esStart.client.asInternalUser;
this.logger = logger;
this.soStart = soStart;
this.cloud = cloud;
}
/**
@ -105,9 +101,6 @@ export class PolicyWatcher {
for (const policy of response.items as PolicyData[]) {
const updatePolicy = getPolicyDataForUpdate(policy);
const policyConfig = updatePolicy.inputs[0].config.policy.value;
updatePolicy.inputs[0].config.policy.value.meta.license = license.type || '';
// add cloud info to policy meta
updatePolicy.inputs[0].config.policy.value.meta.cloud = this.cloud?.isCloudEnabled;
try {
if (!isEndpointPolicyValidForLicense(policyConfig, license)) {

View file

@ -366,7 +366,8 @@ describe('ingest_integration tests ', () => {
logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService
endpointAppContextMock.endpointMetadataService,
cloudService
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value = mockPolicy;
@ -382,7 +383,8 @@ describe('ingest_integration tests ', () => {
logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService
endpointAppContextMock.endpointMetadataService,
cloudService
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value = mockPolicy;
@ -412,7 +414,8 @@ describe('ingest_integration tests ', () => {
logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService
endpointAppContextMock.endpointMetadataService,
cloudService
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value = mockPolicy;
@ -427,6 +430,66 @@ describe('ingest_integration tests ', () => {
});
});
describe('package policy update callback when meta fields should be updated', () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
beforeEach(() => {
licenseEmitter.next(Platinum); // set license level to platinum
});
it('updates successfully when meta fields differ from services', async () => {
const mockPolicy = policyFactory();
mockPolicy.meta.cloud = true; // cloud mock will return true
mockPolicy.meta.license = 'platinum'; // license is set to emit platinum
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(
logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService
);
const policyConfig = generator.generatePolicyPackagePolicy();
// values should be updated
policyConfig.inputs[0]!.config!.policy.value.meta.cloud = false;
policyConfig.inputs[0]!.config!.policy.value.meta.license = 'gold';
const updatedPolicyConfig = await callback(
policyConfig,
soClient,
esClient,
requestContextMock.convertContext(ctx),
req
);
expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy);
});
it('meta fields stay the same where there is no difference', async () => {
const mockPolicy = policyFactory();
mockPolicy.meta.cloud = true; // cloud mock will return true
mockPolicy.meta.license = 'platinum'; // license is set to emit platinum
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(
logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService
);
const policyConfig = generator.generatePolicyPackagePolicy();
// values should be updated
policyConfig.inputs[0]!.config!.policy.value.meta.cloud = true;
policyConfig.inputs[0]!.config!.policy.value.meta.license = 'platinum';
const updatedPolicyConfig = await callback(
policyConfig,
soClient,
esClient,
requestContextMock.convertContext(ctx),
req
);
expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy);
});
});
describe('package policy delete callback', () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

View file

@ -44,6 +44,17 @@ const isEndpointPackagePolicy = <T extends { package?: { name: string } }>(
return packagePolicy.package?.name === 'endpoint';
};
const shouldUpdateMetaValues = (
endpointPackagePolicy: PolicyConfig,
currentLicenseType: string,
currentCloudInfo: boolean
) => {
return (
endpointPackagePolicy.meta.license !== currentLicenseType ||
endpointPackagePolicy.meta.cloud !== currentCloudInfo
);
};
/**
* Callback to handle creation of PackagePolicies in Fleet
*/
@ -152,7 +163,8 @@ export const getPackagePolicyUpdateCallback = (
logger: Logger,
licenseService: LicenseService,
featureUsageService: FeatureUsageService,
endpointMetadataService: EndpointMetadataService
endpointMetadataService: EndpointMetadataService,
cloud: CloudSetup
): PutPackagePolicyUpdateCallback => {
return async (newPackagePolicy: NewPackagePolicy): Promise<UpdatePackagePolicy> => {
if (!isEndpointPackagePolicy(newPackagePolicy)) {
@ -170,6 +182,22 @@ export const getPackagePolicyUpdateCallback = (
notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService);
const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy
?.value as PolicyConfig;
if (
newPackagePolicy.inputs[0].config?.policy?.value &&
shouldUpdateMetaValues(
newEndpointPackagePolicy,
licenseService.getLicenseType(),
cloud?.isCloudEnabled
)
) {
newEndpointPackagePolicy.meta.license = licenseService.getLicenseType();
newEndpointPackagePolicy.meta.cloud = cloud?.isCloudEnabled;
newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy;
}
return newPackagePolicy;
};
};

View file

@ -472,7 +472,6 @@ export class Plugin implements ISecuritySolutionPlugin {
plugins.fleet.packagePolicyService,
core.savedObjects,
core.elasticsearch,
plugins.cloud,
logger
);
this.policyWatcher.start(licenseService);