[Security Solution][Endpoint] Add API checks to Endpoint Policy create/update for checking endpointPolicyProtections is enabled (#163429)

## Summary

- Adds checks to both the Policy Create and Policy Update APIs (Fleet
API extension points) and turns off all protections if
`endpointPolicyProtections` appFeature is disabled
- Adds migration of policies to the Plugin `start()` that will check if
`endpointPolicyProtections` is disabled and updates all existing
policies (if necessary) to disable protections.
This commit is contained in:
Paul Tavares 2023-08-11 09:32:25 -04:00 committed by GitHub
parent 2ba659d091
commit 27c394c936
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 715 additions and 71 deletions

View file

@ -6,14 +6,19 @@
*/
import type { PolicyConfig } from '../types';
import { ProtectionModes } from '../types';
import { PolicyOperatingSystem, ProtectionModes } from '../types';
import { policyFactory } from './policy_config';
import { disableProtections } from './policy_config_helpers';
import {
disableProtections,
isPolicySetToEventCollectionOnly,
ensureOnlyEventCollectionIsAllowed,
} from './policy_config_helpers';
import { set } from 'lodash';
describe('Policy Config helpers', () => {
describe('disableProtections', () => {
it('disables all the protections in the default policy', () => {
expect(disableProtections(policyFactory())).toEqual<PolicyConfig>(eventsOnlyPolicy);
expect(disableProtections(policyFactory())).toEqual<PolicyConfig>(eventsOnlyPolicy());
});
it('does not enable supported fields', () => {
@ -51,20 +56,20 @@ describe('Policy Config helpers', () => {
};
const expectedPolicyWithoutSupportedProtections: PolicyConfig = {
...eventsOnlyPolicy,
...eventsOnlyPolicy(),
windows: {
...eventsOnlyPolicy.windows,
...eventsOnlyPolicy().windows,
memory_protection: notSupported,
behavior_protection: notSupportedBehaviorProtection,
ransomware: notSupported,
},
mac: {
...eventsOnlyPolicy.mac,
...eventsOnlyPolicy().mac,
memory_protection: notSupported,
behavior_protection: notSupportedBehaviorProtection,
},
linux: {
...eventsOnlyPolicy.linux,
...eventsOnlyPolicy().linux,
memory_protection: notSupported,
behavior_protection: notSupportedBehaviorProtection,
},
@ -104,10 +109,10 @@ describe('Policy Config helpers', () => {
};
const expectedPolicy: PolicyConfig = {
...eventsOnlyPolicy,
windows: { ...eventsOnlyPolicy.windows, events: { ...windowsEvents } },
mac: { ...eventsOnlyPolicy.mac, events: { ...macEvents } },
linux: { ...eventsOnlyPolicy.linux, events: { ...linuxEvents } },
...eventsOnlyPolicy(),
windows: { ...eventsOnlyPolicy().windows, events: { ...windowsEvents } },
mac: { ...eventsOnlyPolicy().mac, events: { ...macEvents } },
linux: { ...eventsOnlyPolicy().linux, events: { ...linuxEvents } },
};
const inputPolicy = {
@ -120,11 +125,73 @@ describe('Policy Config helpers', () => {
expect(disableProtections(inputPolicy)).toEqual<PolicyConfig>(expectedPolicy);
});
});
describe('setPolicyToEventCollectionOnly()', () => {
it('should set the policy to event collection only', () => {
expect(ensureOnlyEventCollectionIsAllowed(policyFactory())).toEqual(eventsOnlyPolicy());
});
});
describe('isPolicySetToEventCollectionOnly', () => {
let policy: PolicyConfig;
beforeEach(() => {
policy = ensureOnlyEventCollectionIsAllowed(policyFactory());
});
it.each([
{
keyPath: `${PolicyOperatingSystem.windows}.malware.mode`,
keyValue: ProtectionModes.prevent,
expectedResult: false,
},
{
keyPath: `${PolicyOperatingSystem.mac}.malware.mode`,
keyValue: ProtectionModes.off,
expectedResult: true,
},
{
keyPath: `${PolicyOperatingSystem.windows}.ransomware.mode`,
keyValue: ProtectionModes.prevent,
expectedResult: false,
},
{
keyPath: `${PolicyOperatingSystem.linux}.memory_protection.mode`,
keyValue: ProtectionModes.off,
expectedResult: true,
},
{
keyPath: `${PolicyOperatingSystem.mac}.behavior_protection.mode`,
keyValue: ProtectionModes.detect,
expectedResult: false,
},
{
keyPath: `${PolicyOperatingSystem.windows}.attack_surface_reduction.credential_hardening.enabled`,
keyValue: true,
expectedResult: false,
},
{
keyPath: `${PolicyOperatingSystem.windows}.antivirus_registration.enabled`,
keyValue: true,
expectedResult: false,
},
])(
'should return `$expectedResult` if `$keyPath` is set to `$keyValue`',
({ keyPath, keyValue, expectedResult }) => {
set(policy, keyPath, keyValue);
expect(isPolicySetToEventCollectionOnly(policy)).toEqual({
isOnlyCollectingEvents: expectedResult,
message: expectedResult ? undefined : `property [${keyPath}] is set to [${keyValue}]`,
});
}
);
});
});
// This constant makes sure that if the type `PolicyConfig` is ever modified,
// the logic for disabling protections is also modified due to type check.
export const eventsOnlyPolicy: PolicyConfig = {
export const eventsOnlyPolicy = (): PolicyConfig => ({
meta: { license: '', cloud: false, license_uid: '', cluster_name: '', cluster_uuid: '' },
windows: {
events: {
@ -187,4 +254,4 @@ export const eventsOnlyPolicy: PolicyConfig = {
capture_env_vars: 'LD_PRELOAD,LD_LIBRARY_PATH',
},
},
};
});

View file

@ -5,8 +5,63 @@
* 2.0.
*/
import { get, set } from 'lodash';
import type { PolicyConfig } from '../types';
import { ProtectionModes } from '../types';
import { PolicyOperatingSystem, ProtectionModes } from '../types';
interface PolicyProtectionReference {
keyPath: string;
osList: PolicyOperatingSystem[];
enableValue: unknown;
disableValue: unknown;
}
const getPolicyProtectionsReference = (): PolicyProtectionReference[] => {
const allOsValues = [
PolicyOperatingSystem.mac,
PolicyOperatingSystem.linux,
PolicyOperatingSystem.windows,
];
return [
{
keyPath: 'malware.mode',
osList: [...allOsValues],
disableValue: ProtectionModes.off,
enableValue: ProtectionModes.prevent,
},
{
keyPath: 'ransomware.mode',
osList: [PolicyOperatingSystem.windows],
disableValue: ProtectionModes.off,
enableValue: ProtectionModes.prevent,
},
{
keyPath: 'memory_protection.mode',
osList: [...allOsValues],
disableValue: ProtectionModes.off,
enableValue: ProtectionModes.prevent,
},
{
keyPath: 'behavior_protection.mode',
osList: [...allOsValues],
disableValue: ProtectionModes.off,
enableValue: ProtectionModes.prevent,
},
{
keyPath: 'attack_surface_reduction.credential_hardening.enabled',
osList: [PolicyOperatingSystem.windows],
disableValue: false,
enableValue: true,
},
{
keyPath: 'antivirus_registration.enabled',
osList: [PolicyOperatingSystem.windows],
disableValue: false,
enableValue: true,
},
];
};
/**
* Returns a copy of the passed `PolicyConfig` with all protections set to disabled.
@ -106,3 +161,46 @@ const getDisabledWindowsSpecificPopups = (policy: PolicyConfig) => ({
enabled: false,
},
});
/**
* Returns the provided with only event collection turned enabled
* @param policy
*/
export const ensureOnlyEventCollectionIsAllowed = (policy: PolicyConfig): PolicyConfig => {
const updatedPolicy = disableProtections(policy);
set(updatedPolicy, 'windows.antivirus_registration.enabled', false);
return updatedPolicy;
};
/**
* Checks to see if the provided policy is set to Event Collection only
*/
export const isPolicySetToEventCollectionOnly = (
policy: PolicyConfig
): { isOnlyCollectingEvents: boolean; message?: string } => {
const protectionsRef = getPolicyProtectionsReference();
let message: string | undefined;
const hasEnabledProtection = protectionsRef.some(({ keyPath, osList, disableValue }) => {
const hasOsPropertyEnabled = osList.some((osValue) => {
const fullKeyPathForOs = `${osValue}.${keyPath}`;
const currentValue = get(policy, fullKeyPathForOs);
const isEnabled = currentValue !== disableValue;
if (isEnabled) {
message = `property [${fullKeyPathForOs}] is set to [${currentValue}]`;
}
return isEnabled;
});
return hasOsPropertyEnabled;
});
return {
isOnlyCollectingEvents: !hasEnabledProtection,
message,
};
};

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
export type PromiseResolvedValue<T extends Promise<any>> = T extends Promise<infer Value>
? Value
: never;

View file

@ -17,6 +17,7 @@ import type {
import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types';
import type { AppFeatures } from '../lib/app_features';
import {
getPackagePolicyCreateCallback,
getPackagePolicyUpdateCallback,
@ -69,6 +70,7 @@ export interface EndpointAppContextServiceStartContract {
actionCreateService: ActionCreateService | undefined;
cloud: CloudSetup;
esClient: ElasticsearchClient;
appFeatures: AppFeatures;
}
/**
@ -106,6 +108,7 @@ export class EndpointAppContextService {
featureUsageService,
endpointMetadataService,
esClient,
appFeatures,
} = dependencies;
registerIngestCallback(
@ -117,7 +120,8 @@ export class EndpointAppContextService {
alerting,
licenseService,
exceptionListsClient,
cloud
cloud,
appFeatures
)
);
@ -134,7 +138,8 @@ export class EndpointAppContextService {
featureUsageService,
endpointMetadataService,
cloud,
esClient
esClient,
appFeatures
)
);

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 { createMockEndpointAppContextServiceStartContract } from '../mocks';
import type { Logger } from '@kbn/logging';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { EndpointInternalFleetServicesInterface } from '../services/fleet';
import type { AppFeatures } from '../../lib/app_features';
import { createAppFeaturesMock } from '../../lib/app_features/mocks';
import { ALL_APP_FEATURE_KEYS } from '../../../common';
import { turnOffPolicyProtectionsIfNotSupported } from './turn_off_policy_protections';
import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator';
import type { PolicyData } from '../../../common/endpoint/types';
import type { PackagePolicyClient } from '@kbn/fleet-plugin/server';
import type { PromiseResolvedValue } from '../../../common/endpoint/types/utility_types';
import { ensureOnlyEventCollectionIsAllowed } from '../../../common/endpoint/models/policy_config_helpers';
describe('Turn Off Policy Protections Migration', () => {
let esClient: ElasticsearchClient;
let fleetServices: EndpointInternalFleetServicesInterface;
let appFeatures: AppFeatures;
let logger: Logger;
const callTurnOffPolicyProtections = () =>
turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, appFeatures, logger);
beforeEach(() => {
const endpointContextStartContract = createMockEndpointAppContextServiceStartContract();
({ esClient, appFeatures, logger } = endpointContextStartContract);
fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser();
});
describe('and `endpointPolicyProtections` is enabled', () => {
it('should do nothing', async () => {
await callTurnOffPolicyProtections();
expect(fleetServices.packagePolicy.list as jest.Mock).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenLastCalledWith(
'App feature [endpoint_policy_protections] is enabled. Nothing to do!'
);
});
});
describe('and `endpointPolicyProtections` is disabled', () => {
let policyGenerator: FleetPackagePolicyGenerator;
let page1Items: PolicyData[] = [];
let page2Items: PolicyData[] = [];
let bulkUpdateResponse: PromiseResolvedValue<ReturnType<PackagePolicyClient['bulkUpdate']>>;
const generatePolicyMock = (withDisabledProtections = false): PolicyData => {
const policy = policyGenerator.generateEndpointPackagePolicy();
if (!withDisabledProtections) {
return policy;
}
policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(
policy.inputs[0].config.policy.value
);
return policy;
};
beforeEach(() => {
policyGenerator = new FleetPackagePolicyGenerator('seed');
const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
appFeatures = createAppFeaturesMock(
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections')
);
page1Items = [generatePolicyMock(), generatePolicyMock(true)];
page2Items = [generatePolicyMock(true), generatePolicyMock()];
packagePolicyListSrv
.mockImplementationOnce(async () => {
return {
total: 1500,
page: 1,
perPage: 1000,
items: page1Items,
};
})
.mockImplementationOnce(async () => {
return {
total: 1500,
page: 2,
perPage: 1000,
items: page2Items,
};
});
bulkUpdateResponse = {
updatedPolicies: [page1Items[0], page2Items[1]],
failedPolicies: [],
};
(fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => {
return bulkUpdateResponse;
});
});
it('should update only policies that have protections turn on', async () => {
await callTurnOffPolicyProtections();
expect(fleetServices.packagePolicy.list as jest.Mock).toHaveBeenCalledTimes(2);
expect(fleetServices.packagePolicy.bulkUpdate as jest.Mock).toHaveBeenCalledWith(
fleetServices.internalSoClient,
esClient,
[
expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![0].id }),
expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![1].id }),
],
{ user: { username: 'elastic' } }
);
expect(logger.info).toHaveBeenCalledWith(
'Found 2 policies that need updates:\n' +
`Policy [${bulkUpdateResponse.updatedPolicies![0].id}][${
bulkUpdateResponse.updatedPolicies![0].name
}] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]\n` +
`Policy [${bulkUpdateResponse.updatedPolicies![1].id}][${
bulkUpdateResponse.updatedPolicies![1].name
}] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]`
);
expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully');
});
it('should log failures', async () => {
bulkUpdateResponse.failedPolicies.push({
error: new Error('oh oh'),
packagePolicy: bulkUpdateResponse.updatedPolicies![0],
});
await callTurnOffPolicyProtections();
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Done. 1 out of 2 failed to update:')
);
});
});
});

View file

@ -0,0 +1,107 @@
/*
* 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 { Logger, ElasticsearchClient } from '@kbn/core/server';
import type { UpdatePackagePolicy } from '@kbn/fleet-plugin/common';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import {
isPolicySetToEventCollectionOnly,
ensureOnlyEventCollectionIsAllowed,
} from '../../../common/endpoint/models/policy_config_helpers';
import type { PolicyData } from '../../../common/endpoint/types';
import { AppFeatureSecurityKey } from '../../../common/types/app_features';
import type { EndpointInternalFleetServicesInterface } from '../services/fleet';
import type { AppFeatures } from '../../lib/app_features';
import { getPolicyDataForUpdate } from '../../../common/endpoint/service/policy';
export const turnOffPolicyProtectionsIfNotSupported = async (
esClient: ElasticsearchClient,
fleetServices: EndpointInternalFleetServicesInterface,
appFeaturesService: AppFeatures,
logger: Logger
): Promise<void> => {
const log = logger.get('endpoint', 'policyProtections');
if (appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) {
log.info(
`App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is enabled. Nothing to do!`
);
return;
}
log.info(
`App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is disabled. Checking endpoint integration policies for compliance`
);
const { packagePolicy, internalSoClient, endpointPolicyKuery } = fleetServices;
const updates: UpdatePackagePolicy[] = [];
const messages: string[] = [];
const perPage = 1000;
let hasMoreData = true;
let total = 0;
let page = 1;
do {
const currentPage = page++;
const { items, total: totalPolicies } = await packagePolicy.list(internalSoClient, {
page: currentPage,
kuery: endpointPolicyKuery,
perPage,
});
total = totalPolicies;
hasMoreData = currentPage * perPage < total;
for (const item of items) {
const integrationPolicy = item as PolicyData;
const policySettings = integrationPolicy.inputs[0].config.policy.value;
const { message, isOnlyCollectingEvents } = isPolicySetToEventCollectionOnly(policySettings);
if (!isOnlyCollectingEvents) {
messages.push(
`Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to disable protections. Trigger: [${message}]`
);
integrationPolicy.inputs[0].config.policy.value =
ensureOnlyEventCollectionIsAllowed(policySettings);
updates.push({
...getPolicyDataForUpdate(integrationPolicy),
id: integrationPolicy.id,
});
}
}
} while (hasMoreData);
if (updates.length > 0) {
log.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`);
const bulkUpdateResponse = await fleetServices.packagePolicy.bulkUpdate(
internalSoClient,
esClient,
updates,
{
user: { username: 'elastic' } as AuthenticatedUser,
}
);
log.debug(`Bulk update response:\n${JSON.stringify(bulkUpdateResponse, null, 2)}`);
if (bulkUpdateResponse.failedPolicies.length > 0) {
log.error(
`Done. ${bulkUpdateResponse.failedPolicies.length} out of ${
updates.length
} failed to update:\n${JSON.stringify(bulkUpdateResponse.failedPolicies, null, 2)}`
);
} else {
log.info(`Done. All updates applied successfully`);
}
} else {
log.info(`Done. Checked ${total} policies and no updates needed`);
}
};

View file

@ -71,6 +71,7 @@ import type { EndpointAuthz } from '../../common/endpoint/types/authz';
import { EndpointFleetServicesFactory } from './services/fleet';
import { createLicenseServiceMock } from '../../common/license/mocks';
import { createFeatureUsageServiceMock } from './services/feature_usage/mocks';
import { createAppFeaturesMock } from '../lib/app_features/mocks';
/**
* Creates a mocked EndpointAppContext.
@ -163,6 +164,8 @@ export const createMockEndpointAppContextServiceStartContract =
},
savedObjectsStart
);
const experimentalFeatures = config.experimentalFeatures;
const appFeatures = createAppFeaturesMock(undefined, experimentalFeatures, undefined, logger);
packagePolicyService.list.mockImplementation(async (_, options) => {
return {
@ -207,11 +210,12 @@ export const createMockEndpointAppContextServiceStartContract =
cases: casesMock,
cloud: cloudMock.createSetup(),
featureUsageService: createFeatureUsageServiceMock(),
experimentalFeatures: createMockConfig().experimentalFeatures,
experimentalFeatures,
messageSigningService: createMessageSigningServiceMock(),
actionCreateService: undefined,
createFleetActionsClient: jest.fn((_) => fleetActionsClientMock),
esClient: elasticsearchClientMock.createElasticsearchClient(),
appFeatures,
};
};

View file

@ -13,6 +13,8 @@ import type {
PackagePolicyClient,
PackageClient,
} from '@kbn/fleet-plugin/server';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { createInternalSoClient } from '../../utils/create_internal_so_client';
import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client';
export interface EndpointFleetServicesFactoryInterface {
@ -42,7 +44,10 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
packages: packageService.asInternalUser,
packagePolicy,
endpointPolicyKuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "endpoint"`,
internalReadonlySoClient: createInternalReadonlySoClient(this.savedObjectsStart),
internalSoClient: createInternalSoClient(this.savedObjectsStart),
};
}
}
@ -55,6 +60,8 @@ export interface EndpointFleetServicesInterface {
agentPolicy: AgentPolicyServiceInterface;
packages: PackageClient;
packagePolicy: PackagePolicyClient;
/** The `kuery` that can be used to filter for Endpoint integration policies */
endpointPolicyKuery: string;
}
export interface EndpointInternalFleetServicesInterface extends EndpointFleetServicesInterface {
@ -62,4 +69,7 @@ export interface EndpointInternalFleetServicesInterface extends EndpointFleetSer
* An internal SO client (readonly) that can be used with the Fleet services that require it
*/
internalReadonlySoClient: SavedObjectsClientContract;
/** Internal SO client. USE ONLY WHEN ABSOLUTELY NEEDED. Else, use the `internalReadonlySoClient` */
internalSoClient: SavedObjectsClientContract;
}

View file

@ -5,12 +5,8 @@
* 2.0.
*/
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type {
KibanaRequest,
SavedObjectsClientContract,
SavedObjectsServiceStart,
} from '@kbn/core/server';
import type { SavedObjectsClientContract, SavedObjectsServiceStart } from '@kbn/core/server';
import { createInternalSoClient } from './create_internal_so_client';
import { EndpointError } from '../../../common/endpoint/errors';
type SavedObjectsClientContractKeys = keyof SavedObjectsClientContract;
@ -37,18 +33,7 @@ export class InternalReadonlySoClientMethodNotAllowedError extends EndpointError
export const createInternalReadonlySoClient = (
savedObjectsServiceStart: SavedObjectsServiceStart
): SavedObjectsClientContract => {
const fakeRequest = {
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: { href: {} },
raw: { req: { url: '/' } },
} as unknown as KibanaRequest;
const internalSoClient = savedObjectsServiceStart.getScopedClient(fakeRequest, {
excludedExtensions: [SECURITY_EXTENSION_ID],
});
const internalSoClient = createInternalSoClient(savedObjectsServiceStart);
return new Proxy(internalSoClient, {
get(

View file

@ -0,0 +1,27 @@
/*
* 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 { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server';
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server';
export const createInternalSoClient = (
savedObjectsServiceStart: SavedObjectsServiceStart
): SavedObjectsClientContract => {
const fakeRequest = {
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: { href: {} },
raw: { req: { url: '/' } },
} as unknown as KibanaRequest;
return savedObjectsServiceStart.getScopedClient(fakeRequest, {
excludedExtensions: [SECURITY_EXTENSION_ID],
});
};

View file

@ -54,6 +54,9 @@ import { createMockPolicyData } from '../endpoint/services/feature_usage/mocks';
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../common/endpoint/service/artifacts/constants';
import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { disableProtections } from '../../common/endpoint/models/policy_config_helpers';
import type { AppFeatures } from '../lib/app_features';
import { createAppFeaturesMock } from '../lib/app_features/mocks';
import { ALL_APP_FEATURE_KEYS } from '../../common';
jest.mock('uuid', () => ({
v4: (): string => 'NEW_UUID',
@ -74,6 +77,7 @@ describe('ingest_integration tests ', () => {
});
const generator = new EndpointDocGenerator();
const cloudService = cloudMock.createSetup();
let appFeatures: AppFeatures;
beforeEach(() => {
endpointAppContextMock = createMockEndpointAppContextServiceStartContract();
@ -82,6 +86,7 @@ describe('ingest_integration tests ', () => {
licenseEmitter = new Subject();
licenseService = new LicenseService();
licenseService.start(licenseEmitter);
appFeatures = endpointAppContextMock.appFeatures;
jest
.spyOn(endpointAppContextMock.endpointMetadataService, 'getFleetEndpointPackagePolicy')
@ -129,7 +134,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.alerting,
licenseService,
exceptionListClient,
cloudService
cloudService,
appFeatures
);
return callback(
@ -363,6 +369,7 @@ describe('ingest_integration tests ', () => {
);
});
});
describe('package policy update callback (when the license is below platinum)', () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
@ -379,7 +386,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient
esClient,
appFeatures
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value = mockPolicy;
@ -397,7 +405,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient
esClient,
appFeatures
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value = mockPolicy;
@ -419,6 +428,7 @@ describe('ingest_integration tests ', () => {
beforeEach(() => {
licenseEmitter.next(Platinum); // set license level to platinum
});
it('updates successfully when paid features are turned on', async () => {
const mockPolicy = policyFactory();
mockPolicy.windows.popup.malware.message = 'paid feature';
@ -429,7 +439,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient
esClient,
appFeatures
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value = mockPolicy;
@ -442,6 +453,50 @@ describe('ingest_integration tests ', () => {
);
expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy);
});
it('should turn off protections if endpointPolicyProtections appFeature is disabled', async () => {
appFeatures = createAppFeaturesMock(
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections')
);
const callback = getPackagePolicyUpdateCallback(
endpointAppContextMock.logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient,
appFeatures
);
const updatedPolicy = await callback(
generator.generatePolicyPackagePolicy(),
soClient,
esClient,
requestContextMock.convertContext(ctx),
req
);
expect(updatedPolicy.inputs?.[0]?.config?.policy.value).toMatchObject({
linux: {
behavior_protection: { mode: 'off' },
malware: { mode: 'off' },
memory_protection: { mode: 'off' },
},
mac: {
behavior_protection: { mode: 'off' },
malware: { mode: 'off' },
memory_protection: { mode: 'off' },
},
windows: {
antivirus_registration: { enabled: false },
attack_surface_reduction: { credential_hardening: { enabled: false } },
behavior_protection: { mode: 'off' },
malware: { blocklist: false },
memory_protection: { mode: 'off' },
ransomware: { mode: 'off' },
},
});
});
});
describe('package policy update callback when meta fields should be updated', () => {
@ -486,7 +541,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient
esClient,
appFeatures
);
const policyConfig = generator.generatePolicyPackagePolicy();
@ -520,7 +576,8 @@ describe('ingest_integration tests ', () => {
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient
esClient,
appFeatures
);
const policyConfig = generator.generatePolicyPackagePolicy();
// values should be updated

View file

@ -22,6 +22,12 @@ import type {
} from '@kbn/fleet-plugin/common';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types';
import { AppFeatureSecurityKey } from '../../common/types/app_features';
import {
isPolicySetToEventCollectionOnly,
ensureOnlyEventCollectionIsAllowed,
} from '../../common/endpoint/models/policy_config_helpers';
import type { AppFeatures } from '../lib/app_features';
import type { NewPolicyData, PolicyConfig } from '../../common/endpoint/types';
import type { LicenseService } from '../../common/license';
import type { ManifestManager } from '../endpoint/services';
@ -72,7 +78,8 @@ export const getPackagePolicyCreateCallback = (
alerts: AlertsStartContract,
licenseService: LicenseService,
exceptionsClient: ExceptionListClient | undefined,
cloud: CloudSetup
cloud: CloudSetup,
appFeatures: AppFeatures
): PostPackagePolicyCreateCallback => {
return async (
newPackagePolicy,
@ -140,7 +147,8 @@ export const getPackagePolicyCreateCallback = (
licenseService,
endpointIntegrationConfig,
cloud,
esClientInfo
esClientInfo,
appFeatures
);
return {
@ -175,31 +183,38 @@ export const getPackagePolicyUpdateCallback = (
featureUsageService: FeatureUsageService,
endpointMetadataService: EndpointMetadataService,
cloud: CloudSetup,
esClient: ElasticsearchClient
esClient: ElasticsearchClient,
appFeatures: AppFeatures
): PutPackagePolicyUpdateCallback => {
return async (newPackagePolicy: NewPackagePolicy): Promise<UpdatePackagePolicy> => {
if (!isEndpointPackagePolicy(newPackagePolicy)) {
return newPackagePolicy;
}
const endpointIntegrationData = newPackagePolicy as NewPolicyData;
// Validate that Endpoint Security policy is valid against current license
validatePolicyAgainstLicense(
// The cast below is needed in order to ensure proper typing for
// the policy configuration specific for endpoint
newPackagePolicy.inputs[0].config?.policy?.value as PolicyConfig,
endpointIntegrationData.inputs[0].config?.policy?.value as PolicyConfig,
licenseService,
logger
);
notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService);
notifyProtectionFeatureUsage(
endpointIntegrationData,
featureUsageService,
endpointMetadataService
);
const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy
const newEndpointPackagePolicy = endpointIntegrationData.inputs[0].config?.policy
?.value as PolicyConfig;
const esClientInfo: InfoResponse = await esClient.info();
if (
newPackagePolicy.inputs[0].config?.policy?.value &&
endpointIntegrationData.inputs[0].config?.policy?.value &&
shouldUpdateMetaValues(
newEndpointPackagePolicy,
licenseService.getLicenseType(),
@ -214,10 +229,25 @@ export const getPackagePolicyUpdateCallback = (
newEndpointPackagePolicy.meta.cluster_name = esClientInfo.cluster_name;
newEndpointPackagePolicy.meta.cluster_uuid = esClientInfo.cluster_uuid;
newEndpointPackagePolicy.meta.license_uid = licenseService.getLicenseUID();
newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy;
endpointIntegrationData.inputs[0].config.policy.value = newEndpointPackagePolicy;
}
return newPackagePolicy;
// If no Policy Protection allowed (ex. serverless)
const eventsOnlyPolicy = isPolicySetToEventCollectionOnly(newEndpointPackagePolicy);
if (
!appFeatures.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections) &&
!eventsOnlyPolicy.isOnlyCollectingEvents
) {
logger.warn(
`Endpoint integration policy [${endpointIntegrationData.id}][${endpointIntegrationData.name}] adjusted due to [endpointPolicyProtections] appFeature not being enabled. Trigger [${eventsOnlyPolicy.message}]`
);
endpointIntegrationData.inputs[0].config.policy.value =
ensureOnlyEventCollectionIsAllowed(newEndpointPackagePolicy);
}
return endpointIntegrationData;
};
};

View file

@ -19,6 +19,9 @@ import type {
PolicyCreateCloudConfig,
PolicyCreateEndpointConfig,
} from '../types';
import type { AppFeatures } from '../../lib/app_features';
import { createAppFeaturesMock } from '../../lib/app_features/mocks';
import { ALL_APP_FEATURE_KEYS } from '../../../common';
describe('Create Default Policy tests ', () => {
const cloud = cloudMock.createSetup();
@ -28,6 +31,7 @@ describe('Create Default Policy tests ', () => {
const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold', uid: '' } });
let licenseEmitter: Subject<ILicense>;
let licenseService: LicenseService;
let appFeatures: AppFeatures;
const createDefaultPolicyCallback = async (
config: AnyPolicyCreateConfig | undefined
@ -35,7 +39,7 @@ describe('Create Default Policy tests ', () => {
const esClientInfo = await elasticsearchServiceMock.createClusterClient().asInternalUser.info();
esClientInfo.cluster_name = '';
esClientInfo.cluster_uuid = '';
return createDefaultPolicy(licenseService, config, cloud, esClientInfo);
return createDefaultPolicy(licenseService, config, cloud, esClientInfo, appFeatures);
};
beforeEach(() => {
@ -43,7 +47,9 @@ describe('Create Default Policy tests ', () => {
licenseService = new LicenseService();
licenseService.start(licenseEmitter);
licenseEmitter.next(Platinum); // set license level to platinum
appFeatures = createAppFeaturesMock();
});
describe('When no config is set', () => {
it('Should return PolicyConfig for events only when license is at least platinum', async () => {
const defaultPolicy = policyFactory();
@ -174,6 +180,7 @@ describe('Create Default Policy tests ', () => {
});
});
});
it('Should return process, file and network events enabled when preset is EDR Essential', async () => {
const config = createEndpointConfig({ preset: 'EDREssential' });
const policy = await createDefaultPolicyCallback(config);
@ -190,6 +197,7 @@ 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);
@ -199,7 +207,37 @@ describe('Create Default Policy tests ', () => {
defaultPolicy.meta.cloud = true;
expect(policy).toMatchObject(defaultPolicy);
});
it('should set policy to event collection only if endpointPolicyProtections appFeature is disabled', async () => {
appFeatures = createAppFeaturesMock(
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections')
);
await expect(
createDefaultPolicyCallback(createEndpointConfig({ preset: 'EDRComplete' }))
).resolves.toMatchObject({
linux: {
behavior_protection: { mode: 'off' },
malware: { mode: 'off' },
memory_protection: { mode: 'off' },
},
mac: {
behavior_protection: { mode: 'off' },
malware: { mode: 'off' },
memory_protection: { mode: 'off' },
},
windows: {
antivirus_registration: { enabled: false },
attack_surface_reduction: { credential_hardening: { enabled: false } },
behavior_protection: { mode: 'off' },
malware: { blocklist: false },
memory_protection: { mode: 'off' },
ransomware: { mode: 'off' },
},
});
});
});
describe('When cloud config is set', () => {
const createCloudConfig = (): PolicyCreateCloudConfig => ({
type: 'cloud',

View file

@ -7,6 +7,8 @@
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types';
import { AppFeatureSecurityKey } from '../../../common/types/app_features';
import type { AppFeatures } from '../../lib/app_features';
import {
policyFactory as policyConfigFactory,
policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures,
@ -20,7 +22,10 @@ import {
ENDPOINT_CONFIG_PRESET_NGAV,
ENDPOINT_CONFIG_PRESET_DATA_COLLECTION,
} from '../constants';
import { disableProtections } from '../../../common/endpoint/models/policy_config_helpers';
import {
disableProtections,
ensureOnlyEventCollectionIsAllowed,
} from '../../../common/endpoint/models/policy_config_helpers';
/**
* Create the default endpoint policy based on the current license and configuration type
@ -29,7 +34,8 @@ export const createDefaultPolicy = (
licenseService: LicenseService,
config: AnyPolicyCreateConfig | undefined,
cloud: CloudSetup,
esClientInfo: InfoResponse
esClientInfo: InfoResponse,
appFeatures: AppFeatures
): PolicyConfig => {
const factoryPolicy = policyConfigFactory();
@ -44,15 +50,21 @@ export const createDefaultPolicy = (
: factoryPolicy.meta.cluster_uuid;
factoryPolicy.meta.license_uid = licenseService.getLicenseUID();
const defaultPolicyPerType =
let defaultPolicyPerType: PolicyConfig =
config?.type === 'cloud'
? getCloudPolicyConfig(factoryPolicy)
: getEndpointPolicyWithIntegrationConfig(factoryPolicy, config);
// Apply license limitations in the final step, so it's not overriden (see malware popup)
return licenseService.isPlatinumPlus()
? defaultPolicyPerType
: policyConfigFactoryWithoutPaidFeatures(defaultPolicyPerType);
if (!licenseService.isPlatinumPlus()) {
defaultPolicyPerType = policyConfigFactoryWithoutPaidFeatures(defaultPolicyPerType);
}
// If no Policy Protection allowed (ex. serverless)
if (!appFeatures.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) {
defaultPolicyPerType = ensureOnlyEventCollectionIsAllowed(defaultPolicyPerType);
}
return defaultPolicyPerType;
};
/**

View file

@ -59,7 +59,7 @@ export class AppFeatures {
return this.appFeatures.has(appFeatureKey);
}
private registerEnabledKibanaFeatures() {
protected registerEnabledKibanaFeatures() {
if (this.featuresSetup == null) {
throw new Error(
'Cannot sync kibana features as featuresSetup is not present. Did you call init?'

View file

@ -0,0 +1,38 @@
/*
* 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 { Logger } from '@kbn/core/server';
import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
import { AppFeatures } from './app_features';
import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common';
import { ALL_APP_FEATURE_KEYS, allowedExperimentalValues } from '../../../common';
class AppFeaturesMock extends AppFeatures {
protected registerEnabledKibanaFeatures() {
// NOOP
}
}
export const createAppFeaturesMock = (
/** What features keys should be enabled. Default is all */
enabledFeatureKeys: AppFeatureKeys = [...ALL_APP_FEATURE_KEYS],
experimentalFeatures: ExperimentalFeatures = { ...allowedExperimentalValues },
featuresPluginSetupContract: FeaturesPluginSetup = featuresPluginMock.createSetup(),
logger: Logger = loggingSystemMock.create().get('appFeatureMock')
) => {
const appFeatures = new AppFeaturesMock(logger, experimentalFeatures);
appFeatures.init(featuresPluginSetupContract);
if (enabledFeatureKeys) {
appFeatures.set(enabledFeatureKeys);
}
return appFeatures;
};

View file

@ -17,6 +17,7 @@ import { Dataset } from '@kbn/rule-registry-plugin/server';
import type { ListPluginSetup } from '@kbn/lists-plugin/server';
import type { ILicense } from '@kbn/licensing-plugin/server';
import { turnOffPolicyProtectionsIfNotSupported } from './endpoint/migrations/turn_off_policy_protections';
import { endpointSearchStrategyProvider } from './search_strategy/endpoint';
import { getScheduleNotificationResponseActionsService } from './lib/detection_engine/rule_response_actions/schedule_notification_response_actions';
import { siemGuideId, siemGuideConfig } from '../common/guided_onboarding/siem_guide_config';
@ -438,6 +439,15 @@ export class Plugin implements ISecuritySolutionPlugin {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
plugins.fleet!;
let manifestManager: ManifestManager | undefined;
const endpointFleetServicesFactory = new EndpointFleetServicesFactory(
{
agentService,
packageService,
packagePolicyService,
agentPolicyService,
},
core.savedObjects
);
this.licensing$ = plugins.licensing.license$;
@ -459,17 +469,23 @@ export class Plugin implements ISecuritySolutionPlugin {
esClient: core.elasticsearch.client.asInternalUser,
});
// Migrate artifacts to fleet and then start the minifest task after that is done
// Migrate artifacts to fleet and then start the manifest task after that is done
plugins.fleet.fleetSetupCompleted().then(() => {
logger.info('Dependent plugin setup complete - Starting ManifestTask');
if (this.manifestTask) {
logger.info('Dependent plugin setup complete - Starting ManifestTask');
this.manifestTask.start({
taskManager,
});
} else {
logger.error(new Error('User artifacts task not available.'));
}
turnOffPolicyProtectionsIfNotSupported(
core.elasticsearch.client.asInternalUser,
endpointFleetServicesFactory.asInternalUser(),
this.appFeatures,
logger
);
});
// License related start
@ -493,15 +509,7 @@ export class Plugin implements ISecuritySolutionPlugin {
packagePolicyService,
logger
),
endpointFleetServicesFactory: new EndpointFleetServicesFactory(
{
agentService,
packageService,
packagePolicyService,
agentPolicyService,
},
core.savedObjects
),
endpointFleetServicesFactory,
security: plugins.security,
alerting: plugins.alerting,
config: this.config,
@ -522,6 +530,7 @@ export class Plugin implements ISecuritySolutionPlugin {
),
createFleetActionsClient,
esClient: core.elasticsearch.client.asInternalUser,
appFeatures: this.appFeatures,
});
this.telemetryReceiver.start(