[8.x] [Defend Workflows][Staged Artifact Rollout] Propagate global telemetry config to endpoint policies (#207106) (#208095)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Defend Workflows][Staged Artifact Rollout] Propagate global
telemetry config to endpoint policies
(#207106)](https://github.com/elastic/kibana/pull/207106)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Gergő
Ábrahám","email":"gergo.abraham@elastic.co"},"sourceCommit":{"committedDate":"2025-01-23T15:26:18Z","message":"[Defend
Workflows][Staged Artifact Rollout] Propagate global telemetry config to
endpoint policies (#207106)\n\n## Summary\r\n\r\n\r\nThe goal to
propagate `isOptedIn` from the Telemetry plugin to Endpoint\r\npolicies
as a field named `global_telemetry_config`.\r\n\r\nFor this:\r\n-
telemetry plugin is marked as required for security solution\r\n- a new
`TelemetryConfigProvider` subscribes to the Observable, and\r\npersists
the pushed value to functions that need it\r\n(note: here we could have
used the already public `getIsOptedIn()`, but\r\nthat's now deprecated,
and we need the Observable for watching changes\r\nanyway)\r\n- the
config value is added to newly created policies\r\n- a new
`TelemetryConfigWatcher` is subscribed to the `isOptedIn# Backport

This will backport the following commits from `main` to `8.x`:
- [[Defend Workflows][Staged Artifact Rollout] Propagate global
telemetry config to endpoint policies
(#207106)](https://github.com/elastic/kibana/pull/207106)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT \r\nobservable, and updates package policies
accordingly\r\n- as this is performed on Kibana startup, it acts as a
backfill\r\nfunctionality as well, no need for adding a
backfill/migration \r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies
following conditions. \r\n\r\nReviewers should verify this PR satisfies
this list as well.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"c579b8e4412b85ebae31f9d6bb1d4e677455fd2f","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Defend
Workflows","backport:prev-minor"],"title":"[Defend Workflows][Staged
Artifact Rollout] Propagate global telemetry config to endpoint
policies","number":207106,"url":"https://github.com/elastic/kibana/pull/207106","mergeCommit":{"message":"[Defend
Workflows][Staged Artifact Rollout] Propagate global telemetry config to
endpoint policies (#207106)\n\n## Summary\r\n\r\n\r\nThe goal to
propagate `isOptedIn` from the Telemetry plugin to Endpoint\r\npolicies
as a field named `global_telemetry_config`.\r\n\r\nFor this:\r\n-
telemetry plugin is marked as required for security solution\r\n- a new
`TelemetryConfigProvider` subscribes to the Observable, and\r\npersists
the pushed value to functions that need it\r\n(note: here we could have
used the already public `getIsOptedIn()`, but\r\nthat's now deprecated,
and we need the Observable for watching changes\r\nanyway)\r\n- the
config value is added to newly created policies\r\n- a new
`TelemetryConfigWatcher` is subscribed to the `isOptedIn# Backport

This will backport the following commits from `main` to `8.x`:
- [[Defend Workflows][Staged Artifact Rollout] Propagate global
telemetry config to endpoint policies
(#207106)](https://github.com/elastic/kibana/pull/207106)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT \r\nobservable, and updates package policies
accordingly\r\n- as this is performed on Kibana startup, it acts as a
backfill\r\nfunctionality as well, no need for adding a
backfill/migration \r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies
following conditions. \r\n\r\nReviewers should verify this PR satisfies
this list as well.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"c579b8e4412b85ebae31f9d6bb1d4e677455fd2f"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/207106","number":207106,"mergeCommit":{"message":"[Defend
Workflows][Staged Artifact Rollout] Propagate global telemetry config to
endpoint policies (#207106)\n\n## Summary\r\n\r\n\r\nThe goal to
propagate `isOptedIn` from the Telemetry plugin to Endpoint\r\npolicies
as a field named `global_telemetry_config`.\r\n\r\nFor this:\r\n-
telemetry plugin is marked as required for security solution\r\n- a new
`TelemetryConfigProvider` subscribes to the Observable, and\r\npersists
the pushed value to functions that need it\r\n(note: here we could have
used the already public `getIsOptedIn()`, but\r\nthat's now deprecated,
and we need the Observable for watching changes\r\nanyway)\r\n- the
config value is added to newly created policies\r\n- a new
`TelemetryConfigWatcher` is subscribed to the `isOptedIn# Backport

This will backport the following commits from `main` to `8.x`:
- [[Defend Workflows][Staged Artifact Rollout] Propagate global
telemetry config to endpoint policies
(#207106)](https://github.com/elastic/kibana/pull/207106)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT \r\nobservable, and updates package policies
accordingly\r\n- as this is performed on Kibana startup, it acts as a
backfill\r\nfunctionality as well, no need for adding a
backfill/migration \r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies
following conditions. \r\n\r\nReviewers should verify this PR satisfies
this list as well.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"c579b8e4412b85ebae31f9d6bb1d4e677455fd2f"}}]}]
BACKPORT-->
This commit is contained in:
Gergő Ábrahám 2025-01-23 18:45:33 +01:00 committed by GitHub
parent 7dba4975fe
commit 8a5f3ecede
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 512 additions and 55 deletions

View file

@ -13,24 +13,26 @@ import { isBillablePolicy } from './policy_config_helpers';
/**
* Return a new default `PolicyConfig` for platinum and above licenses
*/
export const policyFactory = (
export const policyFactory = ({
license = '',
cloud = false,
licenseUid = '',
licenseUuid = '',
clusterUuid = '',
clusterName = '',
serverless = false
): PolicyConfig => {
serverless = false,
isGlobalTelemetryEnabled = false,
} = {}): PolicyConfig => {
const policy: PolicyConfig = {
meta: {
license,
license_uuid: licenseUid,
license_uuid: licenseUuid,
cluster_uuid: clusterUuid,
cluster_name: clusterName,
cloud,
serverless,
},
global_manifest_version: 'latest',
global_telemetry_enabled: isGlobalTelemetryEnabled,
windows: {
events: {
credential_access: true,

View file

@ -333,6 +333,7 @@ describe('Policy Config helpers', () => {
// the logic for disabling protections is also modified due to type check.
export const eventsOnlyPolicy = (): PolicyConfig => ({
global_manifest_version: 'latest',
global_telemetry_enabled: false,
meta: {
license: '',
cloud: false,

View file

@ -973,6 +973,7 @@ export interface PolicyConfig {
heartbeatinterval?: number;
};
global_manifest_version: 'latest' | string;
global_telemetry_enabled: boolean;
windows: {
advanced?: {
[key: string]: unknown;

View file

@ -0,0 +1,15 @@
/*
* 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 { TelemetryConfigProvider } from './telemetry_config_provider';
export const createTelemetryConfigProviderMock = (): jest.Mocked<TelemetryConfigProvider> => ({
start: jest.fn(),
stop: jest.fn(),
getObservable: jest.fn(),
getIsOptedIn: jest.fn().mockReturnValue(true),
});

View file

@ -0,0 +1,48 @@
/*
* 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 { Observable } from 'rxjs';
import { TelemetryConfigProvider } from './telemetry_config_provider';
describe('TelemetryConfigProvider', () => {
let telemetryConfigProvider: TelemetryConfigProvider;
beforeEach(() => {
telemetryConfigProvider = new TelemetryConfigProvider();
});
describe('getIsOptedIn()', () => {
it('returns undefined when object is uninitialized', () => {
expect(telemetryConfigProvider.getIsOptedIn()).toBe(undefined);
});
it.each([true, false])('returns pushed %s value after subscribed', (value) => {
const observable$ = new Observable<boolean>((subscriber) => {
subscriber.next(value);
});
telemetryConfigProvider.start(observable$);
expect(telemetryConfigProvider.getIsOptedIn()).toBe(value);
});
});
it('stop() unsubscribes from Observable', async () => {
const unsubscribeMock = jest.fn();
const observableMock = {
subscribe: () => ({
unsubscribe: unsubscribeMock,
}),
} as unknown as Observable<boolean>;
telemetryConfigProvider.start(observableMock);
expect(unsubscribeMock).not.toBeCalled();
telemetryConfigProvider.stop();
expect(unsubscribeMock).toBeCalled();
});
});

View file

@ -0,0 +1,34 @@
/*
* 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 { Observable, Subscription } from 'rxjs';
export class TelemetryConfigProvider {
private isOptedIn$?: Observable<boolean>;
private _isOptedIn?: boolean;
private subscription?: Subscription;
public start(isOptedIn$: Observable<boolean>) {
this.isOptedIn$ = isOptedIn$;
this.subscription = this.isOptedIn$.subscribe((isOptedIn) => {
this._isOptedIn = isOptedIn;
});
}
public stop() {
this.subscription?.unsubscribe();
}
public getIsOptedIn() {
return this._isOptedIn;
}
public getObservable() {
return this.isOptedIn$;
}
}

View file

@ -60,7 +60,8 @@
"charts",
"entityManager",
"inference",
"productDocBase"
"productDocBase",
"telemetry"
],
"optionalPlugins": [
"encryptedSavedObjects",
@ -72,7 +73,6 @@
"lists",
"home",
"management",
"telemetry",
"dataViewFieldEditor",
"osquery",
"savedObjectsTaggingOss",
@ -95,4 +95,4 @@
"common"
]
}
}
}

View file

@ -272,6 +272,7 @@ describe('policy details: ', () => {
policy: {
value: {
global_manifest_version: 'latest',
global_telemetry_enabled: false,
meta: {
license: '',
cloud: false,

View file

@ -26,6 +26,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types';
import type { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import type { TelemetryConfigProvider } from '../../common/telemetry_config/telemetry_config_provider';
import { SavedObjectsClientFactory } from './services/saved_objects';
import type { ResponseActionsClient } from './services';
import { getResponseActionsClient, NormalizedExternalConnectorClient } from './services';
@ -86,6 +87,7 @@ export interface EndpointAppContextServiceStartContract {
productFeaturesService: ProductFeaturesService;
savedObjectsServiceStart: SavedObjectsServiceStart;
connectorActions: ActionsPluginStartContract;
telemetryConfigProvider: TelemetryConfigProvider;
}
/**
@ -154,6 +156,7 @@ export class EndpointAppContextService {
manifestManager,
alerting,
licenseService,
telemetryConfigProvider,
exceptionListsClient,
featureUsageService,
esClient,
@ -184,7 +187,8 @@ export class EndpointAppContextService {
licenseService,
exceptionListsClient,
this.setupDependencies.cloud,
productFeaturesService
productFeaturesService,
telemetryConfigProvider
)
);

View file

@ -22,7 +22,9 @@ import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks';
import { policyFactory } from '../../../../common/endpoint/models/policy_config';
import type { PolicyConfig } from '../../../../common/endpoint/types';
const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): PackagePolicy => {
const MockPackagePolicyWithEndpointPolicy = (
cb?: (p: PolicyConfig) => PolicyConfig
): PackagePolicy => {
const packagePolicy = createPackagePolicyMock();
if (!cb) {
// eslint-disable-next-line no-param-reassign
@ -30,6 +32,7 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa
}
const policyConfig = cb(policyFactory());
packagePolicy.inputs[0].config = { policy: { value: policyConfig } };
return packagePolicy;
};
@ -78,19 +81,19 @@ describe('Policy-Changing license watcher', () => {
// set up the mocked package policy service to return and do what we want
packagePolicySvcMock.list
.mockResolvedValueOnce({
items: Array.from({ length: 100 }, () => MockPPWithEndpointPolicy()),
items: Array.from({ length: 100 }, () => MockPackagePolicyWithEndpointPolicy()),
total: TOTAL,
page: 1,
perPage: 100,
})
.mockResolvedValueOnce({
items: Array.from({ length: 100 }, () => MockPPWithEndpointPolicy()),
items: Array.from({ length: 100 }, () => MockPackagePolicyWithEndpointPolicy()),
total: TOTAL,
page: 2,
perPage: 100,
})
.mockResolvedValueOnce({
items: Array.from({ length: TOTAL - 200 }, () => MockPPWithEndpointPolicy()),
items: Array.from({ length: TOTAL - 200 }, () => MockPackagePolicyWithEndpointPolicy()),
total: TOTAL,
page: 3,
perPage: 100,
@ -113,7 +116,7 @@ describe('Policy-Changing license watcher', () => {
// mock a Policy with a higher-tiered feature enabled
packagePolicySvcMock.list.mockResolvedValueOnce({
items: [
MockPPWithEndpointPolicy((pc: PolicyConfig): PolicyConfig => {
MockPackagePolicyWithEndpointPolicy((pc: PolicyConfig): PolicyConfig => {
pc.windows.popup.malware.message = CustomMessage;
return pc;
}),

View file

@ -0,0 +1,145 @@
/*
* 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 { Subject } from 'rxjs';
import { elasticsearchServiceMock, savedObjectsServiceMock } from '@kbn/core/server/mocks';
import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks';
import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
import type { PackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common';
import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks';
import { policyFactory } from '../../../../common/endpoint/models/policy_config';
import type { PolicyConfig } from '../../../../common/endpoint/types';
import { TelemetryConfigWatcher } from './telemetry_watch';
import { TelemetryConfigProvider } from '../../../../common/telemetry_config/telemetry_config_provider';
import { createMockEndpointAppContextService } from '../../mocks';
const MockPackagePolicyWithEndpointPolicy = (
cb?: (p: PolicyConfig) => PolicyConfig
): PackagePolicy => {
const packagePolicy = createPackagePolicyMock();
if (!cb) {
// eslint-disable-next-line no-param-reassign
cb = (p) => p;
}
const policyConfig = cb(policyFactory());
packagePolicy.inputs[0].config = { policy: { value: policyConfig } };
return packagePolicy;
};
describe('Telemetry config watcher', () => {
const soStartMock = savedObjectsServiceMock.createStartContract();
const esStartMock = elasticsearchServiceMock.createStart();
let packagePolicySvcMock: jest.Mocked<PackagePolicyClient>;
let telemetryWatcher: TelemetryConfigWatcher;
const preparePackagePolicyMock = ({
isGlobalTelemetryEnabled,
}: {
isGlobalTelemetryEnabled: boolean;
}) => {
packagePolicySvcMock.list.mockResolvedValueOnce({
items: [
MockPackagePolicyWithEndpointPolicy((pc: PolicyConfig): PolicyConfig => {
pc.global_telemetry_enabled = isGlobalTelemetryEnabled;
return pc;
}),
],
total: 1,
page: 1,
perPage: 100,
});
};
beforeEach(() => {
packagePolicySvcMock = createPackagePolicyServiceMock();
telemetryWatcher = new TelemetryConfigWatcher(
packagePolicySvcMock,
soStartMock,
esStartMock,
createMockEndpointAppContextService()
);
});
it('is activated on telemetry config changes', () => {
const telemetryConfigEmitter: Subject<boolean> = new Subject();
const telemetryConfigProvider = new TelemetryConfigProvider();
// spy on the watch() function
const mockWatch = jest.fn();
telemetryWatcher.watch = mockWatch;
telemetryConfigProvider.start(telemetryConfigEmitter);
telemetryWatcher.start(telemetryConfigProvider);
telemetryConfigEmitter.next(true);
expect(mockWatch).toBeCalledTimes(1);
telemetryWatcher.stop();
telemetryConfigProvider.stop();
telemetryConfigEmitter.complete();
});
it('pages through all endpoint policies', async () => {
const TOTAL = 247;
// set up the mocked package policy service to return and do what we want
packagePolicySvcMock.list
.mockResolvedValueOnce({
items: Array.from({ length: 100 }, () => MockPackagePolicyWithEndpointPolicy()),
total: TOTAL,
page: 1,
perPage: 100,
})
.mockResolvedValueOnce({
items: Array.from({ length: 100 }, () => MockPackagePolicyWithEndpointPolicy()),
total: TOTAL,
page: 2,
perPage: 100,
})
.mockResolvedValueOnce({
items: Array.from({ length: TOTAL - 200 }, () => MockPackagePolicyWithEndpointPolicy()),
total: TOTAL,
page: 3,
perPage: 100,
});
await telemetryWatcher.watch(true); // manual trigger
expect(packagePolicySvcMock.list).toBeCalledTimes(3);
// Assert: on the first call to packagePolicy.list, we asked for page 1
expect(packagePolicySvcMock.list.mock.calls[0][1].page).toBe(1);
expect(packagePolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2
expect(packagePolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc
});
it.each([true, false])(
'does not update policies if both global telemetry config and policy fields are %s',
async (value) => {
preparePackagePolicyMock({ isGlobalTelemetryEnabled: value });
await telemetryWatcher.watch(value);
expect(packagePolicySvcMock.bulkUpdate).not.toHaveBeenCalled();
}
);
it.each([true, false])('updates `global_telemetry_config` field to %s', async (value) => {
preparePackagePolicyMock({ isGlobalTelemetryEnabled: !value });
await telemetryWatcher.watch(value);
expect(packagePolicySvcMock.bulkUpdate).toHaveBeenCalled();
const policyUpdates: UpdatePackagePolicy[] = packagePolicySvcMock.bulkUpdate.mock.calls[0][2];
expect(policyUpdates.length).toBe(1);
const updatedPolicyConfigs: PolicyConfig = policyUpdates[0].inputs[0].config?.policy.value;
expect(updatedPolicyConfigs.global_telemetry_enabled).toBe(value);
});
});

View file

@ -0,0 +1,140 @@
/*
* 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 { Subscription } from 'rxjs';
import type {
ElasticsearchClient,
ElasticsearchServiceStart,
KibanaRequest,
Logger,
SavedObjectsClientContract,
SavedObjectsServiceStart,
} from '@kbn/core/server';
import type { PackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type { TelemetryConfigProvider } from '../../../../common/telemetry_config/telemetry_config_provider';
import type { PolicyData } from '../../../../common/endpoint/types';
import { getPolicyDataForUpdate } from '../../../../common/endpoint/service/policy';
import type { EndpointAppContextService } from '../../endpoint_app_context_services';
export class TelemetryConfigWatcher {
private logger: Logger;
private esClient: ElasticsearchClient;
private policyService: PackagePolicyClient;
private subscription: Subscription | undefined;
private soStart: SavedObjectsServiceStart;
constructor(
policyService: PackagePolicyClient,
soStart: SavedObjectsServiceStart,
esStart: ElasticsearchServiceStart,
endpointAppContextService: EndpointAppContextService
) {
this.policyService = policyService;
this.esClient = esStart.client.asInternalUser;
this.logger = endpointAppContextService.createLogger(this.constructor.name);
this.soStart = soStart;
}
/**
* The policy watcher is not called as part of a HTTP request chain, where the
* request-scoped SOClient could be passed down. It is called via telemetry observable
* changes. We are acting as the 'system' in response to telemetry changes, so we are
* intentionally using the system user here. Be very aware of what you are using this
* client to do
*/
private makeInternalSOClient(soStart: SavedObjectsServiceStart): SavedObjectsClientContract {
const fakeRequest = {
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: { href: {} },
raw: { req: { url: '/' } },
} as unknown as KibanaRequest;
return soStart.getScopedClient(fakeRequest, { excludedExtensions: [SECURITY_EXTENSION_ID] });
}
public start(telemetryConfigProvider: TelemetryConfigProvider) {
this.subscription = telemetryConfigProvider.getObservable()?.subscribe(this.watch.bind(this));
}
public stop() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
public async watch(isTelemetryEnabled: boolean) {
let page = 1;
let response: {
items: PackagePolicy[];
total: number;
page: number;
perPage: number;
};
this.logger.debug(
`Checking Endpoint policies to update due to changed global telemetry config setting. (New value: ${isTelemetryEnabled})`
);
do {
try {
response = await this.policyService.list(this.makeInternalSOClient(this.soStart), {
page: page++,
perPage: 100,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`,
});
} catch (e) {
this.logger.warn(
`Unable to verify endpoint policies in line with telemetry change: failed to fetch package policies: ${e.message}`
);
return;
}
const updates: UpdatePackagePolicy[] = [];
for (const policy of response.items as PolicyData[]) {
const updatePolicy = getPolicyDataForUpdate(policy);
const policyConfig = updatePolicy.inputs[0].config.policy.value;
if (isTelemetryEnabled !== policyConfig.global_telemetry_enabled) {
policyConfig.global_telemetry_enabled = isTelemetryEnabled;
updates.push({ ...updatePolicy, id: policy.id });
}
}
if (updates.length) {
try {
await this.policyService.bulkUpdate(
this.makeInternalSOClient(this.soStart),
this.esClient,
updates
);
} catch (e) {
// try again for transient issues
try {
await this.policyService.bulkUpdate(
this.makeInternalSOClient(this.soStart),
this.esClient,
updates
);
} catch (ee) {
this.logger.warn(
`Unable to update telemetry config state to ${isTelemetryEnabled} in policies: ${updates.map(
(update) => update.id
)}`
);
this.logger.warn(ee);
}
}
}
} while (response.page * response.perPage < response.total);
}
}

View file

@ -50,6 +50,7 @@ import { unsecuredActionsClientMock } from '@kbn/actions-plugin/server/unsecured
import type { PluginStartContract as ActionPluginStartContract } from '@kbn/actions-plugin/server';
import type { Mutable } from 'utility-types';
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
import { createTelemetryConfigProviderMock } from '../../../common/telemetry_config/mocks';
import { createSavedObjectsClientFactoryMock } from '../services/saved_objects/saved_objects_client_factory.mocks';
import { EndpointMetadataService } from '../services/metadata';
import { createEndpointFleetServicesFactoryMock } from '../services/fleet/endpoint_fleet_services_factory.mocks';
@ -216,6 +217,7 @@ export const createMockEndpointAppContextServiceStartContract =
connectorActions: {
getUnsecuredActionsClient: jest.fn().mockReturnValue(unsecuredActionsClientMock.create()),
} as unknown as jest.Mocked<ActionPluginStartContract>,
telemetryConfigProvider: createTelemetryConfigProviderMock(),
};
return startContract;

View file

@ -83,6 +83,7 @@ import type {
import type { EndpointMetadataService } from '../endpoint/services/metadata';
import { createEndpointMetadataServiceTestContextMock } from '../endpoint/services/metadata/mocks';
import { createPolicyDataStreamsIfNeeded as _createPolicyDataStreamsIfNeeded } from './handlers/create_policy_datastreams';
import { createTelemetryConfigProviderMock } from '../../common/telemetry_config/mocks';
jest.mock('uuid', () => ({
v4: (): string => 'NEW_UUID',
@ -118,6 +119,7 @@ describe('Fleet integrations', () => {
});
const generator = new EndpointDocGenerator();
const cloudService = cloudMock.createSetup();
const telemetryConfigProviderMock = createTelemetryConfigProviderMock();
let productFeaturesService: ProductFeaturesService;
let endpointMetadataService: EndpointMetadataService;
let logger: Logger;
@ -157,7 +159,8 @@ describe('Fleet integrations', () => {
licenseUuid = 'updated-uid',
clusterUuid = '',
clusterName = '',
isServerlessEnabled = cloudService.isServerlessEnabled
isServerlessEnabled = cloudService.isServerlessEnabled,
isTelemetryEnabled = true
) => ({
type: 'endpoint',
enabled: true,
@ -166,14 +169,15 @@ describe('Fleet integrations', () => {
integration_config: {},
policy: {
value: disableProtections(
policyFactory(
policyFactory({
license,
cloud,
licenseUuid,
clusterUuid,
clusterName,
isServerlessEnabled
)
serverless: isServerlessEnabled,
isGlobalTelemetryEnabled: isTelemetryEnabled,
})
),
},
artifact_manifest: { value: manifest },
@ -189,7 +193,8 @@ describe('Fleet integrations', () => {
licenseService,
exceptionListClient,
cloudService,
productFeaturesService
productFeaturesService,
telemetryConfigProviderMock
);
return callback(
@ -365,6 +370,19 @@ describe('Fleet integrations', () => {
isBillablePolicySpy.mockRestore();
});
it.each([false, true])(
'should correctly set `global_telemetry_enabled` to %s',
async (targetValue) => {
const manifestManager = buildManifestManagerMock();
telemetryConfigProviderMock.getIsOptedIn.mockReturnValue(targetValue);
const packagePolicy = await invokeCallback(manifestManager);
const policyConfig: PolicyConfig = packagePolicy.inputs[0].config!.policy.value;
expect(policyConfig.global_telemetry_enabled).toBe(targetValue);
}
);
});
describe('package policy post create callback', () => {

View file

@ -31,6 +31,7 @@ import type {
PostAgentPolicyUpdateCallback,
PutPackagePolicyPostUpdateCallback,
} from '@kbn/fleet-plugin/server/types';
import type { TelemetryConfigProvider } from '../../common/telemetry_config/telemetry_config_provider';
import type { EndpointInternalFleetServicesInterface } from '../endpoint/services/fleet';
import type { EndpointAppContextService } from '../endpoint/endpoint_app_context_services';
import { createPolicyDataStreamsIfNeeded } from './handlers/create_policy_datastreams';
@ -123,7 +124,8 @@ export const getPackagePolicyCreateCallback = (
licenseService: LicenseService,
exceptionsClient: ExceptionListClient | undefined,
cloud: CloudSetup,
productFeatures: ProductFeaturesService
productFeatures: ProductFeaturesService,
telemetryConfigProvider: TelemetryConfigProvider
): PostPackagePolicyCreateCallback => {
return async (
newPackagePolicy,
@ -196,7 +198,8 @@ export const getPackagePolicyCreateCallback = (
endpointIntegrationConfig,
cloud,
esClientInfo,
productFeatures
productFeatures,
telemetryConfigProvider
);
return {

View file

@ -23,6 +23,7 @@ import type {
} from '../types';
import type { ProductFeaturesService } from '../../lib/product_features_service/product_features_service';
import { createProductFeaturesServiceMock } from '../../lib/product_features_service/mocks';
import { createTelemetryConfigProviderMock } from '../../../common/telemetry_config/mocks';
describe('Create Default Policy tests ', () => {
const cloud = cloudMock.createSetup();
@ -33,14 +34,22 @@ describe('Create Default Policy tests ', () => {
let licenseEmitter: Subject<ILicense>;
let licenseService: LicenseService;
let productFeaturesService: ProductFeaturesService;
const telemetryConfigProviderMock = createTelemetryConfigProviderMock();
const createDefaultPolicyCallback = async (
config: AnyPolicyCreateConfig | undefined
config?: AnyPolicyCreateConfig
): Promise<PolicyConfig> => {
const esClientInfo = await elasticsearchServiceMock.createClusterClient().asInternalUser.info();
esClientInfo.cluster_name = '';
esClientInfo.cluster_uuid = '';
return createDefaultPolicy(licenseService, config, cloud, esClientInfo, productFeaturesService);
return createDefaultPolicy(
licenseService,
config,
cloud,
esClientInfo,
productFeaturesService,
telemetryConfigProviderMock
);
};
beforeEach(() => {
@ -202,11 +211,15 @@ describe('Create Default Policy tests ', () => {
it('Should return the default config when preset is EDR Complete', async () => {
const config = createEndpointConfig({ preset: 'EDRComplete' });
const policy = await createDefaultPolicyCallback(config);
const licenseType = 'platinum';
const license = 'platinum';
const isCloud = true;
const defaultPolicy = policyFactory(licenseType, isCloud);
const defaultPolicy = policyFactory({
license,
cloud: isCloud,
isGlobalTelemetryEnabled: true,
});
// update defaultPolicy w/ platinum license & cloud info
defaultPolicy.meta.license = licenseType;
defaultPolicy.meta.license = license;
defaultPolicy.meta.cloud = isCloud;
expect(policy).toMatchObject(defaultPolicy);
});
@ -279,4 +292,22 @@ describe('Create Default Policy tests ', () => {
expect(policy.windows.ransomware.mode).toBe('off');
});
});
describe('Global Telemetry Config', () => {
it('should save telemetry config state in policy based on telemetry config provider', async () => {
telemetryConfigProviderMock.getIsOptedIn.mockReturnValue(false);
let policyConfig = await createDefaultPolicyCallback();
expect(policyConfig.global_telemetry_enabled).toBe(false);
telemetryConfigProviderMock.getIsOptedIn.mockReturnValue(true);
policyConfig = await createDefaultPolicyCallback();
expect(policyConfig.global_telemetry_enabled).toBe(true);
});
it('should fallback to `false` when global telemetry config is unavailable', async () => {
telemetryConfigProviderMock.getIsOptedIn.mockReturnValue(undefined);
const policyConfig = await createDefaultPolicyCallback();
expect(policyConfig.global_telemetry_enabled).toBe(false);
});
});
});

View file

@ -8,6 +8,7 @@
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types';
import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import type { TelemetryConfigProvider } from '../../../common/telemetry_config/telemetry_config_provider';
import {
policyFactory as policyConfigFactory,
policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures,
@ -36,17 +37,19 @@ export const createDefaultPolicy = (
config: AnyPolicyCreateConfig | undefined,
cloud: CloudSetup,
esClientInfo: InfoResponse,
productFeatures: ProductFeaturesService
productFeatures: ProductFeaturesService,
telemetryConfigProvider: TelemetryConfigProvider
): PolicyConfig => {
// Pass license and cloud information to use in Policy creation
const factoryPolicy = policyConfigFactory(
licenseService.getLicenseType(),
cloud?.isCloudEnabled,
licenseService.getLicenseUID(),
esClientInfo?.cluster_uuid,
esClientInfo?.cluster_name,
cloud?.isServerlessEnabled
);
const factoryPolicy = policyConfigFactory({
license: licenseService.getLicenseType(),
cloud: cloud?.isCloudEnabled,
licenseUuid: licenseService.getLicenseUID(),
clusterUuid: esClientInfo?.cluster_uuid,
clusterName: esClientInfo?.cluster_name,
serverless: cloud?.isServerlessEnabled,
isGlobalTelemetryEnabled: telemetryConfigProvider.getIsOptedIn(),
});
let defaultPolicyPerType: PolicyConfig =
config?.type === 'cloud'

View file

@ -130,6 +130,8 @@ import { getCriblPackagePolicyPostCreateOrUpdateCallback } from './security_inte
import { scheduleEntityAnalyticsMigration } from './lib/entity_analytics/migrations';
import { SiemMigrationsService } from './lib/siem_migrations/siem_migrations_service';
import { registerRiskScoreModulesDeprecation } from './deprecations/register_risk_score_modules_deprecation';
import { TelemetryConfigProvider } from '../common/telemetry_config/telemetry_config_provider';
import { TelemetryConfigWatcher } from './endpoint/lib/policy/telemetry_watch';
export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';
@ -150,6 +152,8 @@ export class Plugin implements ISecuritySolutionPlugin {
private lists: ListPluginSetup | undefined; // TODO: can we create ListPluginStart?
private licensing$!: Observable<ILicense>;
private policyWatcher?: PolicyWatcher;
private telemetryConfigProvider: TelemetryConfigProvider;
private telemetryWatcher?: TelemetryConfigWatcher;
private manifestTask: ManifestTask | undefined;
private completeExternalResponseActionsTask: CompleteExternalResponseActionsTask;
@ -179,7 +183,8 @@ export class Plugin implements ISecuritySolutionPlugin {
this.asyncTelemetryEventsSender = new AsyncTelemetryEventsSender(this.logger);
this.telemetryReceiver = new TelemetryReceiver(this.logger);
this.logger.debug('plugin initialized');
this.telemetryConfigProvider = new TelemetryConfigProvider();
this.endpointContext = {
logFactory: this.pluginContext.logger,
service: this.endpointAppContextService,
@ -192,6 +197,8 @@ export class Plugin implements ISecuritySolutionPlugin {
this.completeExternalResponseActionsTask = new CompleteExternalResponseActionsTask({
endpointAppContext: this.endpointContext,
});
this.logger.debug('plugin initialized');
}
public setup(
@ -580,6 +587,8 @@ export class Plugin implements ISecuritySolutionPlugin {
this.licensing$ = plugins.licensing.license$;
this.telemetryConfigProvider.start(plugins.telemetry.isOptedIn$);
// Assistant Tool and Feature Registration
plugins.elasticAssistant.registerTools(APP_UI_ID, assistantTools);
const features = {
@ -611,6 +620,7 @@ export class Plugin implements ISecuritySolutionPlugin {
cases: plugins.cases,
manifestManager,
licenseService,
telemetryConfigProvider: this.telemetryConfigProvider,
exceptionListsClient: exceptionListClient,
registerListsServerExtension: this.lists?.registerExtension,
featureUsageService,
@ -662,6 +672,14 @@ export class Plugin implements ISecuritySolutionPlugin {
logger
);
this.policyWatcher.start(licenseService);
this.telemetryWatcher = new TelemetryConfigWatcher(
plugins.fleet.packagePolicyService,
core.savedObjects,
core.elasticsearch,
this.endpointContext.service
);
this.telemetryWatcher.start(this.telemetryConfigProvider);
}
if (plugins.taskManager) {

View file

@ -83,7 +83,7 @@ export interface SecuritySolutionPluginStartDependencies {
security: SecurityPluginStart;
spaces?: SpacesPluginStart;
taskManager?: TaskManagerPluginStart;
telemetry?: TelemetryPluginStart;
telemetry: TelemetryPluginStart;
share: SharePluginStart;
actions: ActionsPluginStartContract;
inference: InferenceServerStart;

View file

@ -86,12 +86,8 @@ describe('setEndpointPackagePolicyServerlessBillingFlags', () => {
});
it('does NOT update serverless flag for endpoint policies with the flag already set', async () => {
const packagePolicy1 = generatePackagePolicy(
policyFactory(undefined, undefined, undefined, undefined, undefined, true)
);
const packagePolicy2 = generatePackagePolicy(
policyFactory(undefined, undefined, undefined, undefined, undefined, true)
);
const packagePolicy1 = generatePackagePolicy(policyFactory({ serverless: true }));
const packagePolicy2 = generatePackagePolicy(policyFactory({ serverless: true }));
packagePolicyServiceMock.list.mockResolvedValue({
items: [packagePolicy1, packagePolicy2],
page: 1,
@ -116,23 +112,15 @@ describe('setEndpointPackagePolicyServerlessBillingFlags', () => {
it('correctly updates billable flag for endpoint policies', async () => {
// billable: false - serverless false
const packagePolicy1 = generatePackagePolicy(
policyFactory(undefined, undefined, undefined, undefined, undefined, false)
);
const packagePolicy1 = generatePackagePolicy(policyFactory({ serverless: false }));
// billable: true - serverless + protections
const packagePolicy2 = generatePackagePolicy(
policyFactory(undefined, undefined, undefined, undefined, undefined, true)
);
const packagePolicy2 = generatePackagePolicy(policyFactory({ serverless: true }));
// billable: false - serverless true but event collection only
const packagePolicy3 = generatePackagePolicy(
ensureOnlyEventCollectionIsAllowed(
policyFactory(undefined, undefined, undefined, undefined, undefined, true)
)
ensureOnlyEventCollectionIsAllowed(policyFactory({ serverless: true }))
);
// ignored since flag already set
const packagePolicy4 = generatePackagePolicy(
policyFactory(undefined, undefined, undefined, undefined, undefined, true)
);
const packagePolicy4 = generatePackagePolicy(policyFactory({ serverless: true }));
packagePolicyServiceMock.list.mockResolvedValue({
items: [packagePolicy1, packagePolicy2, packagePolicy3, packagePolicy4],
page: 1,