mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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'.

This commit is contained in:
parent
9872b70a84
commit
57374ab80d
17 changed files with 571 additions and 27 deletions
|
@ -44,6 +44,11 @@ export enum AppFeatureSecurityKey {
|
|||
*/
|
||||
osqueryAutomatedResponseActions = 'osquery_automated_response_actions',
|
||||
|
||||
/**
|
||||
* Enables Agent Tamper Protection
|
||||
*/
|
||||
endpointProtectionUpdates = 'endpoint_protection_updates',
|
||||
|
||||
/**
|
||||
* Enables Agent Tamper Protection
|
||||
*/
|
||||
|
|
|
@ -106,6 +106,7 @@ export const securityDefaultAppFeaturesConfig: DefaultSecurityAppFeaturesConfig
|
|||
},
|
||||
|
||||
[AppFeatureSecurityKey.osqueryAutomatedResponseActions]: {},
|
||||
[AppFeatureSecurityKey.endpointProtectionUpdates]: {},
|
||||
[AppFeatureSecurityKey.endpointAgentTamperProtection]: {},
|
||||
[AppFeatureSecurityKey.externalRuleActions]: {},
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ export type UpsellingSectionId =
|
|||
| 'entity_analytics_panel'
|
||||
| 'endpointPolicyProtections'
|
||||
| 'osquery_automated_response_actions'
|
||||
| 'endpoint_protection_updates'
|
||||
| 'endpoint_agent_tamper_protection'
|
||||
| 'ruleDetailsEndpointExceptions';
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -36,6 +36,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
|
|||
AppFeatureKey.osqueryAutomatedResponseActions,
|
||||
AppFeatureKey.endpointAgentTamperProtection,
|
||||
AppFeatureKey.endpointExceptions,
|
||||
AppFeatureKey.endpointProtectionUpdates,
|
||||
],
|
||||
},
|
||||
cloud: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue