[EDR Workflows][Serverless] Gate Protection Updates (#175129)

With this pull request, we implement Protection Updates gating on the
Security Essentials tier. The changes include:

1. Addition of an upselling component on the Protection Updates tab.
2. Extension of the package policy create/update API callback to verify
the protection updates app feature before committing changes to
global_manifest_version.
3. Extension of the turn_off_policy_protections plugin callback to
inspect the protection updates app feature on server start. If no app
feature is present, it will roll down global_manifest_version to the
default of 'latest'.

![Screenshot 2024-01-18 at 15 50
32](a018562f-e528-4f29-a070-57b3b20c949f)
This commit is contained in:
Konrad Szwarc 2024-02-01 20:09:55 +01:00 committed by GitHub
parent 9872b70a84
commit 57374ab80d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 571 additions and 27 deletions

View file

@ -44,6 +44,11 @@ export enum AppFeatureSecurityKey {
*/
osqueryAutomatedResponseActions = 'osquery_automated_response_actions',
/**
* Enables Agent Tamper Protection
*/
endpointProtectionUpdates = 'endpoint_protection_updates',
/**
* Enables Agent Tamper Protection
*/

View file

@ -106,6 +106,7 @@ export const securityDefaultAppFeaturesConfig: DefaultSecurityAppFeaturesConfig
},
[AppFeatureSecurityKey.osqueryAutomatedResponseActions]: {},
[AppFeatureSecurityKey.endpointProtectionUpdates]: {},
[AppFeatureSecurityKey.endpointAgentTamperProtection]: {},
[AppFeatureSecurityKey.externalRuleActions]: {},
};

View file

@ -15,6 +15,7 @@ export type UpsellingSectionId =
| 'entity_analytics_panel'
| 'endpointPolicyProtections'
| 'osquery_automated_response_actions'
| 'endpoint_protection_updates'
| 'endpoint_agent_tamper_protection'
| 'ruleDetailsEndpointExceptions';

View file

@ -0,0 +1,71 @@
/*
* 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 { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { login } from '../../../../tasks/login';
import { loadPage } from '../../../../tasks/common';
import { APP_POLICIES_PATH } from '../../../../../../../common/constants';
describe(
'When displaying the Policy Details in Endpoint Essentials PLI',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
let loadedPolicyData: IndexedFleetEndpointPolicyResponse;
let policyId: string;
before(() => {
cy.task(
'indexFleetEndpointPolicy',
{ policyName: 'tests-serverless' },
{ timeout: 5 * 60 * 1000 }
).then((res) => {
const response = res as IndexedFleetEndpointPolicyResponse;
loadedPolicyData = response;
policyId = response.integrationPolicies[0].id;
});
});
after(() => {
if (loadedPolicyData) {
cy.task('deleteIndexedFleetEndpointPolicies', loadedPolicyData);
}
});
beforeEach(() => {
login();
});
it('should display upselling section for protection updates', () => {
loadPage(`${APP_POLICIES_PATH}/${policyId}/protectionUpdates`);
[
'endpointPolicy-protectionUpdatesLockedCard-title',
'endpointPolicy-protectionUpdatesLockedCard',
'endpointPolicy-protectionUpdatesLockedCard-badge',
].forEach((testSubj) => {
cy.getByTestSubj(testSubj).should('not.exist');
});
[
'protection-updates-warning-callout',
'protection-updates-automatic-updates-enabled',
'protection-updates-manifest-switch',
'protection-updates-manifest-name-title',
].forEach((testSubj) => {
cy.getByTestSubj(testSubj).should('exist').and('be.visible');
});
});
}
);

View file

@ -0,0 +1,64 @@
/*
* 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 { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { login } from '../../../../tasks/login';
import { loadPage } from '../../../../tasks/common';
import { APP_POLICIES_PATH } from '../../../../../../../common/constants';
describe(
'When displaying the Policy Details in Endpoint Essentials PLI',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
let loadedPolicyData: IndexedFleetEndpointPolicyResponse;
let policyId: string;
before(() => {
cy.task(
'indexFleetEndpointPolicy',
{ policyName: 'tests-serverless' },
{ timeout: 5 * 60 * 1000 }
).then((res) => {
const response = res as IndexedFleetEndpointPolicyResponse;
loadedPolicyData = response;
policyId = response.integrationPolicies[0].id;
});
});
after(() => {
if (loadedPolicyData) {
cy.task('deleteIndexedFleetEndpointPolicies', loadedPolicyData);
}
});
beforeEach(() => {
login();
});
it('should display upselling section for protection updates', () => {
loadPage(`${APP_POLICIES_PATH}/${policyId}/protectionUpdates`);
[
'endpointPolicy-protectionUpdatesLockedCard-title',
'endpointPolicy-protectionUpdatesLockedCard',
'endpointPolicy-protectionUpdatesLockedCard-badge',
].forEach((testSubj) => {
cy.getByTestSubj(testSubj, { timeout: 60000 }).should('exist').and('be.visible');
});
cy.getByTestSubj('protection-updates-layout').should('not.exist');
});
}
);

View file

@ -5,9 +5,11 @@
* 2.0.
*/
import { loadPage } from '../../tasks/common';
import { login } from '../../tasks/login';
import { visitPolicyDetailsPage } from '../../screens/policy_details';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { APP_POLICIES_PATH } from '../../../../../common/constants';
describe(
'When displaying the Policy Details in Security Essentials PLI',
@ -21,14 +23,17 @@ describe(
},
() => {
let loadedPolicyData: IndexedFleetEndpointPolicyResponse;
let policyId: string;
before(() => {
cy.task(
'indexFleetEndpointPolicy',
{ policyName: 'tests-serverless' },
{ timeout: 5 * 60 * 1000 }
).then((response) => {
loadedPolicyData = response as IndexedFleetEndpointPolicyResponse;
).then((res) => {
const response = res as IndexedFleetEndpointPolicyResponse;
loadedPolicyData = response;
policyId = response.integrationPolicies[0].id;
});
});
@ -40,13 +45,24 @@ describe(
beforeEach(() => {
login();
visitPolicyDetailsPage(loadedPolicyData.integrationPolicies[0].id);
});
it('should display upselling section for protections', () => {
visitPolicyDetailsPage(policyId);
cy.getByTestSubj('endpointPolicy-protectionsLockedCard', { timeout: 60000 })
.should('exist')
.and('be.visible');
});
it('should display upselling section for protection updates', () => {
loadPage(`${APP_POLICIES_PATH}/${policyId}/protectionUpdates`);
[
'endpointPolicy-protectionUpdatesLockedCard-title',
'endpointPolicy-protectionUpdatesLockedCard',
'endpointPolicy-protectionUpdatesLockedCard-badge',
].forEach((testSubj) => {
cy.getByTestSubj(testSubj, { timeout: 60000 }).should('exist').and('be.visible');
});
cy.getByTestSubj('protection-updates-layout').should('not.exist');
});
}
);

View file

@ -0,0 +1,13 @@
/*
* 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 React from 'react';
import { useUpsellingComponent } from '../../../../../../common/hooks/use_upselling';
export const useGetProtectionUpdatesUnavailableComponent = (): React.ComponentType | null => {
return useUpsellingComponent('endpoint_protection_updates');
};

View file

@ -30,6 +30,7 @@ import type { Moment } from 'moment';
import moment from 'moment';
import { cloneDeep } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { useGetProtectionUpdatesUnavailableComponent } from './hooks/use_get_protection_updates_unavailable_component';
import { ProtectionUpdatesBottomBar } from './components/protection_updates_bottom_bar';
import { useCreateProtectionUpdatesNote } from './hooks/use_post_protection_updates_note';
import { useGetProtectionUpdatesNote } from './hooks/use_get_protection_updates_note';
@ -401,6 +402,12 @@ export const ProtectionUpdatesLayout = React.memo<ProtectionUpdatesLayoutProps>(
);
};
const ProtectionUpdatesUpsellingComponent = useGetProtectionUpdatesUnavailableComponent();
if (ProtectionUpdatesUpsellingComponent) {
return <ProtectionUpdatesUpsellingComponent />;
}
return (
<>
<EuiPanel

View file

@ -29,6 +29,32 @@ describe('Turn Off Policy Protections Migration', () => {
const callTurnOffPolicyProtections = () =>
turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, appFeatureService, logger);
const generatePolicyMock = (
policyGenerator: FleetPackagePolicyGenerator,
withDisabledProtections = false,
withDisabledProtectionUpdates = true
): PolicyData => {
const policy = policyGenerator.generateEndpointPackagePolicy();
if (!withDisabledProtections && withDisabledProtectionUpdates) {
return policy;
} else if (!withDisabledProtections && !withDisabledProtectionUpdates) {
policy.inputs[0].config.policy.value.global_manifest_version = '2023-01-01';
return policy;
} else if (withDisabledProtections && !withDisabledProtectionUpdates) {
policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(
policy.inputs[0].config.policy.value
);
policy.inputs[0].config.policy.value.global_manifest_version = '2023-01-01';
return policy;
} else {
policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(
policy.inputs[0].config.policy.value
);
return policy; // This is the only one that shouldn't be updated since it has default values for disabled features
}
};
beforeEach(() => {
const endpointContextStartContract = createMockEndpointAppContextServiceStartContract();
@ -38,36 +64,97 @@ describe('Turn Off Policy Protections Migration', () => {
fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser();
});
describe('and `endpointPolicyProtections` is enabled', () => {
describe('and both `endpointPolicyProtections` and `endpointProtectionUpdates` is enabled', () => {
it('should do nothing', async () => {
await callTurnOffPolicyProtections();
expect(fleetServices.packagePolicy.list as jest.Mock).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenLastCalledWith(
expect(logger.info).toHaveBeenNthCalledWith(
1,
'App feature [endpoint_policy_protections] is enabled. Nothing to do!'
);
expect(logger.info).toHaveBeenLastCalledWith(
'App feature [endpoint_protection_updates] is enabled. Nothing to do!'
);
});
});
describe('and `endpointPolicyProtections` is disabled', () => {
describe('and `endpointProtectionUpdates` is disabled but `endpointPolicyProtections` is enabled', () => {
let policyGenerator: FleetPackagePolicyGenerator;
let page1Items: PolicyData[] = [];
let page2Items: PolicyData[] = [];
let bulkUpdateResponse: PromiseResolvedValue<ReturnType<PackagePolicyClient['bulkUpdate']>>;
const generatePolicyMock = (withDisabledProtections = false): PolicyData => {
const policy = policyGenerator.generateEndpointPackagePolicy();
beforeEach(() => {
policyGenerator = new FleetPackagePolicyGenerator('seed');
const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
if (!withDisabledProtections) {
return policy;
}
policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed(
policy.inputs[0].config.policy.value
appFeatureService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_protection_updates')
);
return policy;
};
page1Items = [
generatePolicyMock(policyGenerator, false),
generatePolicyMock(policyGenerator, false, false),
];
page2Items = [
generatePolicyMock(policyGenerator, false, false),
generatePolicyMock(policyGenerator, false),
];
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[1], page2Items[0]],
failedPolicies: [],
};
(fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => {
return bulkUpdateResponse;
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should update only policies that have non default manifest versions set', 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' } }
);
});
});
describe('and `endpointPolicyProtections` is disabled, but `endpointProtectionUpdates` is enabled', () => {
let policyGenerator: FleetPackagePolicyGenerator;
let page1Items: PolicyData[] = [];
let page2Items: PolicyData[] = [];
let bulkUpdateResponse: PromiseResolvedValue<ReturnType<PackagePolicyClient['bulkUpdate']>>;
beforeEach(() => {
policyGenerator = new FleetPackagePolicyGenerator('seed');
@ -77,8 +164,14 @@ describe('Turn Off Policy Protections Migration', () => {
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections')
);
page1Items = [generatePolicyMock(), generatePolicyMock(true)];
page2Items = [generatePolicyMock(true), generatePolicyMock()];
page1Items = [
generatePolicyMock(policyGenerator, false, false),
generatePolicyMock(policyGenerator, true, false),
];
page2Items = [
generatePolicyMock(policyGenerator, true, false),
generatePolicyMock(policyGenerator, false, false),
];
packagePolicyListSrv
.mockImplementationOnce(async () => {
@ -108,6 +201,10 @@ describe('Turn Off Policy Protections Migration', () => {
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should update only policies that have protections turn on', async () => {
await callTurnOffPolicyProtections();
@ -145,4 +242,93 @@ describe('Turn Off Policy Protections Migration', () => {
);
});
});
describe('and both `endpointPolicyProtections` and `endpointProtectionUpdates` is disabled', () => {
let policyGenerator: FleetPackagePolicyGenerator;
let page1Items: PolicyData[] = [];
let page2Items: PolicyData[] = [];
let bulkUpdateResponse: PromiseResolvedValue<ReturnType<PackagePolicyClient['bulkUpdate']>>;
beforeEach(() => {
policyGenerator = new FleetPackagePolicyGenerator('seed');
const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock;
appFeatureService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter(
(key) => key !== 'endpoint_policy_protections' && key !== 'endpoint_protection_updates'
)
);
page1Items = [
generatePolicyMock(policyGenerator),
generatePolicyMock(policyGenerator, true), // This is the only one that shouldn't be updated since it has default values for disabled features
generatePolicyMock(policyGenerator, true, false),
generatePolicyMock(policyGenerator, false, false),
];
page2Items = [
generatePolicyMock(policyGenerator, false, false),
generatePolicyMock(policyGenerator, true, false),
generatePolicyMock(policyGenerator, true), // This is the only one that shouldn't be updated since it has default values for disabled features
generatePolicyMock(policyGenerator),
];
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],
page1Items[2],
page1Items[3],
page2Items[0],
page2Items[1],
page2Items[3],
],
failedPolicies: [],
};
(fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => {
return bulkUpdateResponse;
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should update only policies that have protections and protection updates turned 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 }),
expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![2].id }),
expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![3].id }),
expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![4].id }),
expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![5].id }),
],
{ user: { username: 'elastic' } }
);
});
});
});

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { UpdatePackagePolicy } from '@kbn/fleet-plugin/common';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import {
isPolicySetToEventCollectionOnly,
ensureOnlyEventCollectionIsAllowed,
isPolicySetToEventCollectionOnly,
} from '../../../common/endpoint/models/policy_config_helpers';
import type { PolicyData } from '../../../common/endpoint/types';
import type { EndpointInternalFleetServicesInterface } from '../services/fleet';
@ -26,17 +26,41 @@ export const turnOffPolicyProtectionsIfNotSupported = async (
): Promise<void> => {
const log = logger.get('endpoint', 'policyProtections');
if (appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) {
const isProtectionUpdatesFeatureEnabled = appFeaturesService.isEnabled(
AppFeatureSecurityKey.endpointProtectionUpdates
);
const isPolicyProtectionsEnabled = appFeaturesService.isEnabled(
AppFeatureSecurityKey.endpointPolicyProtections
);
if (isPolicyProtectionsEnabled) {
log.info(
`App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is enabled. Nothing to do!`
);
}
if (isProtectionUpdatesFeatureEnabled) {
log.info(
`App feature [${AppFeatureSecurityKey.endpointProtectionUpdates}] is enabled. Nothing to do!`
);
}
if (isPolicyProtectionsEnabled && isProtectionUpdatesFeatureEnabled) {
return;
}
log.info(
`App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is disabled. Checking endpoint integration policies for compliance`
);
if (!isPolicyProtectionsEnabled) {
log.info(
`App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is disabled. Checking endpoint integration policies for compliance`
);
}
if (!isProtectionUpdatesFeatureEnabled) {
log.info(
`App feature [${AppFeatureSecurityKey.endpointProtectionUpdates}] is disabled. Checking endpoint integration policies for compliance`
);
}
const { packagePolicy, internalSoClient, endpointPolicyKuery } = fleetServices;
const updates: UpdatePackagePolicy[] = [];
@ -61,14 +85,35 @@ export const turnOffPolicyProtectionsIfNotSupported = async (
const integrationPolicy = item as PolicyData;
const policySettings = integrationPolicy.inputs[0].config.policy.value;
const { message, isOnlyCollectingEvents } = isPolicySetToEventCollectionOnly(policySettings);
const shouldDowngradeProtectionUpdates =
!isProtectionUpdatesFeatureEnabled && policySettings.global_manifest_version !== 'latest';
if (!isOnlyCollectingEvents) {
if (!isPolicyProtectionsEnabled && !isOnlyCollectingEvents) {
messages.push(
`Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to disable protections. Trigger: [${message}]`
);
integrationPolicy.inputs[0].config.policy.value =
ensureOnlyEventCollectionIsAllowed(policySettings);
if (shouldDowngradeProtectionUpdates) {
messages.push(
`Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to downgrade protection updates.`
);
}
integrationPolicy.inputs[0].config.policy.value = {
...ensureOnlyEventCollectionIsAllowed(policySettings),
...(shouldDowngradeProtectionUpdates ? { global_manifest_version: 'latest' } : {}),
};
updates.push({
...getPolicyDataForUpdate(integrationPolicy),
id: integrationPolicy.id,
});
} else if (shouldDowngradeProtectionUpdates) {
messages.push(
`Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to downgrade protection updates.`
);
integrationPolicy.inputs[0].config.policy.value.global_manifest_version = 'latest';
updates.push({
...getPolicyDataForUpdate(integrationPolicy),

View file

@ -443,6 +443,28 @@ describe('ingest_integration tests ', () => {
const validDateYesterday = moment.utc().subtract(1, 'day');
it('should throw if endpointProtectionUpdates appFeature is disabled and user modifies global_manifest_version', () => {
appFeaturesService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_protection_updates')
);
const callback = getPackagePolicyUpdateCallback(
endpointAppContextMock.logger,
licenseService,
endpointAppContextMock.featureUsageService,
endpointAppContextMock.endpointMetadataService,
cloudService,
esClient,
appFeaturesService
);
const policyConfig = generator.generatePolicyPackagePolicy();
policyConfig.inputs[0]!.config!.policy.value.global_manifest_version = '2023-01-01';
expect(() =>
callback(policyConfig, soClient, esClient, requestContextMock.convertContext(ctx), req)
).rejects.toThrow(
'To modify protection updates, you must add at least Endpoint Complete to your project.'
);
});
it.each([
{
date: 'invalid',

View file

@ -23,6 +23,7 @@ import type {
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types';
import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import { validatePolicyAgainstAppFeatures } from './handlers/validate_policy_against_app_features';
import { validateEndpointPackagePolicy } from './handlers/validate_endpoint_package_policy';
import {
isPolicySetToEventCollectionOnly,
@ -104,6 +105,7 @@ export const getPackagePolicyCreateCallback = (
}
if (newPackagePolicy?.inputs) {
validatePolicyAgainstAppFeatures(newPackagePolicy.inputs, appFeatures);
validateEndpointPackagePolicy(newPackagePolicy.inputs);
}
// Optional endpoint integration configuration
@ -209,6 +211,9 @@ export const getPackagePolicyUpdateCallback = (
logger
);
// Validate that Endpoint Security policy uses only enabled App Features
validatePolicyAgainstAppFeatures(endpointIntegrationData.inputs, appFeatures);
validateEndpointPackagePolicy(endpointIntegrationData.inputs);
notifyProtectionFeatureUsage(

View file

@ -0,0 +1,31 @@
/*
* 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 { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys';
import type { AppFeaturesService } from '../../lib/app_features_service';
export const validatePolicyAgainstAppFeatures = (
inputs: NewPackagePolicyInput[],
appFeaturesService: AppFeaturesService
): void => {
const input = inputs.find((i) => i.type === 'endpoint');
if (input?.config?.policy?.value?.global_manifest_version) {
const globalManifestVersion = input.config.policy.value.global_manifest_version;
if (
globalManifestVersion !== 'latest' &&
!appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointProtectionUpdates)
) {
const appFeatureError: Error & { statusCode?: number; apiPassThrough?: boolean } = new Error(
'To modify protection updates, you must add at least Endpoint Complete to your project.'
);
appFeatureError.statusCode = 403;
appFeatureError.apiPassThrough = true;
throw appFeatureError;
}
}
};

View file

@ -36,6 +36,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
AppFeatureKey.osqueryAutomatedResponseActions,
AppFeatureKey.endpointAgentTamperProtection,
AppFeatureKey.endpointExceptions,
AppFeatureKey.endpointProtectionUpdates,
],
},
cloud: {

View file

@ -20,6 +20,7 @@ import type { AppFeatureKeyType } from '@kbn/security-solution-features';
import {
EndpointAgentTamperProtectionLazy,
EndpointPolicyProtectionsLazy,
EndpointProtectionUpdatesLazy,
RuleDetailsEndpointExceptionsLazy,
} from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
@ -146,6 +147,11 @@ export const upsellingSections: UpsellingSections = [
pli: AppFeatureKey.endpointExceptions,
component: RuleDetailsEndpointExceptionsLazy,
},
{
id: 'endpoint_protection_updates',
pli: AppFeatureKey.endpointProtectionUpdates,
component: EndpointProtectionUpdatesLazy,
},
];
// Upselling for sections, linked by arbitrary ids

View file

@ -0,0 +1,64 @@
/*
* 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 React, { memo } from 'react';
import { EuiCard, EuiIcon, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from '@emotion/styled';
const CARD_TITLE = i18n.translate(
'xpack.securitySolutionServerless.endpointProtectionUpdates.cardTitle',
{
defaultMessage: 'Protection updates',
}
);
const CARD_MESSAGE = i18n.translate(
'xpack.securitySolutionServerless.endpointProtectionUpdates.cardMessage',
{
defaultMessage:
'To modify protection updates, you must add at least Endpoint Complete to your project.',
}
);
const BADGE_TEXT = i18n.translate(
'xpack.securitySolutionServerless.endpointProtectionUpdates.badgeText',
{
defaultMessage: 'Endpoint Complete',
}
);
const CardDescription = styled.p`
padding: 0 33.3%;
`;
/**
* Component displayed when a given product tier is not allowed to use endpoint policy protections.
*/
export const EndpointProtectionUpdates = memo(() => {
return (
<>
<EuiSpacer size="s" />
<EuiCard
data-test-subj="endpointPolicy-protectionUpdatesLockedCard"
isDisabled={true}
description={false}
icon={<EuiIcon size="xl" type="lock" />}
betaBadgeProps={{
'data-test-subj': 'endpointPolicy-protectionUpdatesLockedCard-badge',
label: BADGE_TEXT,
}}
title={
<h3 data-test-subj="endpointPolicy-protectionUpdatesLockedCard-title">
<strong>{CARD_TITLE}</strong>
</h3>
}
>
<CardDescription>{CARD_MESSAGE}</CardDescription>
</EuiCard>
</>
);
});
EndpointProtectionUpdates.displayName = 'EndpointProtectionUpdates';

View file

@ -19,6 +19,12 @@ export const RuleDetailsEndpointExceptionsLazy = lazy(() =>
}))
);
export const EndpointProtectionUpdatesLazy = lazy(() =>
import('./endpoint_protection_updates').then(({ EndpointProtectionUpdates }) => ({
default: EndpointProtectionUpdates,
}))
);
export const EndpointAgentTamperProtectionLazy = lazy(() =>
import('./endpoint_agent_tamper_protection').then(({ EndpointAgentTamperProtection }) => ({
default: EndpointAgentTamperProtection,