[EDR Workflows][UI] Gate Agent Tamper Protection setting on Agent Policy Settings (#174278)

This PR is part of an effort to limit EDR Workflow features to the
Endpoint Complete tier on serverless and focuses on UI part of gating
Agent Tamper Protection.

Related PRs:
- [Agent Tamper Protection
API](https://github.com/elastic/kibana/pull/174400)
- [Protection updates](https://github.com/elastic/kibana/pull/175129)

Currently, the Agent tamper protection switch is in Fleet's agent policy
settings. Right now (for ESS and Serverless), we only control access to
this field with a license check (platinum). This PR adds an extra check
for AppFeature, which is included in the Complete tier PLI config. We
decided to stick with the existing Fleet privileges for this component,
and no extra changes are needed RBAC wise (confirmed with
@roxana-gheorghe).

Changes:

1. Added `endpointAgentTamperProtection` appFeature and linked it to the
endpoint:complete tier.
2. Made an upselling component and registered it with the Upselling
Service.
3. Passed the upselling component to Fleet using UIExtension.
4. Added Cypress end-to-end coverage for Essentials showing the
upselling component and Complete showing the form component.

![Screenshot 2024-01-24 at 15 52
17](6cdc3197-7bd0-4607-9323-ae4318493653)
This commit is contained in:
Konrad Szwarc 2024-02-01 18:15:55 +01:00 committed by GitHub
parent 04004ddaac
commit 2a030677ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 398 additions and 71 deletions

View file

@ -44,6 +44,11 @@ export enum AppFeatureSecurityKey {
*/
osqueryAutomatedResponseActions = 'osquery_automated_response_actions',
/**
* Enables Agent Tamper Protection
*/
endpointAgentTamperProtection = 'endpoint_agent_tamper_protection',
/**
* Enables managing endpoint exceptions on rules and alerts
*/

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, Suspense } from 'react';
import {
EuiDescribedFormGroup,
EuiFormRow,
@ -35,7 +35,13 @@ import {
DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT,
} from '../../../../../../../common/constants';
import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useStartServices, useConfig, useGetAgentPolicies, useLicense } from '../../../../hooks';
import {
useStartServices,
useConfig,
useGetAgentPolicies,
useLicense,
useUIExtension,
} from '../../../../hooks';
import { AgentPolicyPackageBadge } from '../../../../components';
import { UninstallCommandFlyout } from '../../../../../../components';
@ -67,11 +73,16 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
isEditing = false,
}) => {
const { docLinks } = useStartServices();
const AgentTamperProtectionWrapper = useUIExtension(
'endpoint',
'endpoint-agent-tamper-protection'
);
const config = useConfig();
const maxAgentPoliciesWithInactivityTimeout =
config.developer?.maxAgentPoliciesWithInactivityTimeout ??
DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT;
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const {
dataOutputOptions,
monitoringOutputOptions,
@ -102,6 +113,98 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false);
const policyHasElasticDefend = useMemo(() => hasElasticDefend(agentPolicy), [agentPolicy]);
const AgentTamperProtectionSectionContent = useMemo(
() => (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.tamperingLabel"
defaultMessage="Agent tamper protection"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.tamperingDescription"
defaultMessage="Prevent agents from being uninstalled locally. When enabled, agents can only be uninstalled using an authorization token in the uninstall command. Click { linkName } for the full command."
values={{ linkName: <strong>Get uninstall command</strong> }}
/>
}
>
<EuiSwitch
label={
<>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.tamperingSwitchLabel"
defaultMessage="Prevent agent tampering"
/>
{!policyHasElasticDefend && (
<span data-test-subj="tamperMissingIntegrationTooltip">
<EuiIconTip
type="iInCircle"
color="subdued"
content={i18n.translate(
'xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning',
{
defaultMessage:
'Elastic Defend integration is required to enable this feature',
}
)}
/>
</span>
)}
</>
}
checked={agentPolicy.is_protected ?? false}
onChange={(e) => {
updateAgentPolicy({ is_protected: e.target.checked });
}}
disabled={!policyHasElasticDefend}
data-test-subj="tamperProtectionSwitch"
/>
{agentPolicy.id && (
<>
<EuiSpacer size="s" />
<EuiLink
onClick={() => {
setIsUninstallCommandFlyoutOpen(true);
}}
disabled={!agentPolicy.is_protected || !policyHasElasticDefend}
data-test-subj="uninstallCommandLink"
>
{i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', {
defaultMessage: 'Get uninstall command',
})}
</EuiLink>
</>
)}
</EuiDescribedFormGroup>
),
[agentPolicy.id, agentPolicy.is_protected, policyHasElasticDefend, updateAgentPolicy]
);
const AgentTamperProtectionSection = useMemo(() => {
if (agentTamperProtectionEnabled && licenseService.isPlatinum() && !agentPolicy.is_managed) {
if (AgentTamperProtectionWrapper) {
return (
<Suspense fallback={null}>
<AgentTamperProtectionWrapper.Component>
{AgentTamperProtectionSectionContent}
</AgentTamperProtectionWrapper.Component>
</Suspense>
);
}
return AgentTamperProtectionSectionContent;
}
}, [
agentTamperProtectionEnabled,
licenseService,
agentPolicy.is_managed,
AgentTamperProtectionWrapper,
AgentTamperProtectionSectionContent,
]);
return (
<>
<EuiDescribedFormGroup
@ -293,73 +396,9 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
}}
/>
</EuiDescribedFormGroup>
{agentTamperProtectionEnabled && licenseService.isPlatinum() && !agentPolicy.is_managed && (
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.tamperingLabel"
defaultMessage="Agent tamper protection"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.tamperingDescription"
defaultMessage="Prevent agents from being uninstalled locally. When enabled, agents can only be uninstalled using an authorization token in the uninstall command. Click { linkName } for the full command."
values={{ linkName: <strong>Get uninstall command</strong> }}
/>
}
>
<EuiSwitch
label={
<>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.tamperingSwitchLabel"
defaultMessage="Prevent agent tampering"
/>{' '}
{!policyHasElasticDefend && (
<span data-test-subj="tamperMissingIntegrationTooltip">
<EuiIconTip
type="iInCircle"
color="subdued"
content={i18n.translate(
'xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning',
{
defaultMessage:
'Elastic Defend integration is required to enable this feature',
}
)}
/>
</span>
)}
</>
}
checked={agentPolicy.is_protected ?? false}
onChange={(e) => {
updateAgentPolicy({ is_protected: e.target.checked });
}}
disabled={!policyHasElasticDefend}
data-test-subj="tamperProtectionSwitch"
/>
{agentPolicy.id && (
<>
<EuiSpacer size="s" />
<EuiLink
onClick={() => {
setIsUninstallCommandFlyoutOpen(true);
}}
disabled={!agentPolicy.is_protected || !policyHasElasticDefend}
data-test-subj="uninstallCommandLink"
>
{i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', {
defaultMessage: 'Get uninstall command',
})}
</EuiLink>
</>
)}
</EuiDescribedFormGroup>
)}
{AgentTamperProtectionSection}
<EuiDescribedFormGroup
title={
<h4>

View file

@ -113,6 +113,12 @@ export interface PackagePolicyResponseExtension {
Component: LazyExoticComponent<PackagePolicyResponseExtensionComponent>;
}
export interface EndpointAgentTamperProtectionExtension {
package: string;
view: 'endpoint-agent-tamper-protection';
Component: LazyExoticComponent<ComponentType>;
}
export interface PackageGenericErrorsListExtension {
package: string;
view: 'package-generic-errors-list';
@ -219,4 +225,5 @@ export type UIExtensionPoint =
| PackageAssetsExtension
| PackageGenericErrorsListExtension
| AgentEnrollmentFlyoutFinalStepExtension
| PackagePolicyCreateMultiStepExtension;
| PackagePolicyCreateMultiStepExtension
| EndpointAgentTamperProtectionExtension;

View file

@ -0,0 +1,57 @@
/*
* 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 { login } from '../../../../tasks/login';
import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../../../tasks/fleet';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import type { PolicyData } from '../../../../../../../common/endpoint/types';
import {
checkForAgentTamperProtectionAvailability,
navigateToFleetAgentPolicySettings,
} from '../../../../screens/fleet/agent_settings';
describe(
'Agent Policy Settings - Complete',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
describe('Agent Tamper Protection is available with no upselling component present', () => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
beforeEach(() => {
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];
})
);
login();
});
afterEach(() => {
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
});
it('should display upselling section for protections', () => {
navigateToFleetAgentPolicySettings(policy.policy_id);
checkForAgentTamperProtectionAvailability();
});
});
}
);

View file

@ -0,0 +1,57 @@
/*
* 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 { login } from '../../../../tasks/login';
import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../../../tasks/fleet';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import type { PolicyData } from '../../../../../../../common/endpoint/types';
import {
checkForAgentTamperProtectionAvailability,
navigateToFleetAgentPolicySettings,
} from '../../../../screens/fleet/agent_settings';
describe(
'Agent Policy Settings - Essentials',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
describe('Agent Tamper Protection is unavailable with upselling component present', () => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
beforeEach(() => {
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];
})
);
login();
});
afterEach(() => {
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
});
it('should display upselling section for protections', () => {
navigateToFleetAgentPolicySettings(policy.policy_id);
checkForAgentTamperProtectionAvailability(false);
});
});
}
);

View file

@ -0,0 +1,33 @@
/*
* 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 { loadPage } from '../../tasks/common';
export const navigateToFleetAgentPolicySettings = (policyId: string) => {
loadPage(`/app/fleet/policies/${policyId}/settings`);
};
export const checkForAgentTamperProtectionAvailability = (
isAgentTamperProtectionEnabled = true
) => {
const upsellElementVisibility = isAgentTamperProtectionEnabled ? 'not.exist' : 'exist';
const componentElementVisibility = isAgentTamperProtectionEnabled ? 'exist' : 'not.exist';
cy.getByTestSubj('endpointSecurity-agentTamperProtectionLockedCard-badge').should(
upsellElementVisibility
);
cy.getByTestSubj('endpointSecurity-agentTamperProtectionLockedCard-title').should(
upsellElementVisibility
);
cy.getByTestSubj('endpointPolicy-agentTamperProtectionLockedCard').should(
upsellElementVisibility
);
cy.getByTestSubj('tamperProtectionSwitch').should(componentElementVisibility);
cy.getByTestSubj('tamperMissingIntegrationTooltip').should('not.exist');
cy.getByTestSubj('uninstallCommandLink').should(componentElementVisibility);
};

View file

@ -0,0 +1,19 @@
/*
* 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 { useUpsellingComponent } from '../../../../../common/hooks/use_upselling';
export const EndpointAgentTamperProtectionExtension = memo(({ children }) => {
const Component = useUpsellingComponent('endpoint_agent_tamper_protection');
if (!Component) {
return <>{children}</>;
}
return <Component />;
});
EndpointAgentTamperProtectionExtension.displayName = 'EndpointAgentTamperProtectionExtension';

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 { lazy } from 'react';
import type { FleetUiExtensionGetterOptions } from './types';
export const getLazyEndpointAgentTamperProtectionExtension = ({
coreStart,
depsStart,
services,
}: FleetUiExtensionGetterOptions) =>
lazy(async () => {
const [{ withSecurityContext }, { EndpointAgentTamperProtectionExtension }] = await Promise.all(
[
import('./components/with_security_context/with_security_context'),
import('./endpoint_agent_tamper_protection_extension'),
]
);
return {
default: withSecurityContext({
coreStart,
depsStart,
services,
WrappedComponent: EndpointAgentTamperProtectionExtension,
}),
};
});

View file

@ -21,6 +21,7 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { NowProvider, QueryService } from '@kbn/data-plugin/public';
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { getLazyEndpointAgentTamperProtectionExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_agent_tamper_protection_extension';
import type { FleetUiExtensionGetterOptions } from './management/pages/policy/view/ingest_manager_integration/types';
import type {
PluginSetup,
@ -337,6 +338,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
view: 'package-detail-assets',
Component: LazyEndpointCustomAssetsExtension,
});
registerExtension({
package: 'endpoint',
view: 'endpoint-agent-tamper-protection',
Component: getLazyEndpointAgentTamperProtectionExtension(registerOptions),
});
}
// Not using await to prevent blocking start execution

View file

@ -34,6 +34,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
complete: [
AppFeatureKey.endpointResponseActions,
AppFeatureKey.osqueryAutomatedResponseActions,
AppFeatureKey.endpointAgentTamperProtection,
AppFeatureKey.endpointExceptions,
],
},

View file

@ -18,6 +18,7 @@ import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/me
import { AppFeatureKey } from '@kbn/security-solution-features/keys';
import type { AppFeatureKeyType } from '@kbn/security-solution-features';
import {
EndpointAgentTamperProtectionLazy,
EndpointPolicyProtectionsLazy,
RuleDetailsEndpointExceptionsLazy,
} from './sections/endpoint_management';
@ -130,6 +131,11 @@ export const upsellingSections: UpsellingSections = [
/>
),
},
{
id: 'endpoint_agent_tamper_protection',
pli: AppFeatureKey.endpointAgentTamperProtection,
component: EndpointAgentTamperProtectionLazy,
},
{
id: 'endpointPolicyProtections',
pli: AppFeatureKey.endpointPolicyProtections,

View file

@ -0,0 +1,58 @@
/*
* 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 } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from '@emotion/styled';
const BADGE_TEXT = i18n.translate(
'xpack.securitySolutionServerless.rules.endpointSecurity.agentTamperProtection.badgeText',
{
defaultMessage: 'Endpoint Complete',
}
);
const CARD_TITLE = i18n.translate(
'xpack.securitySolutionServerless.rules.endpointSecurity.agentTamperProtection.cardTitle',
{
defaultMessage: 'Do more with Security!',
}
);
const CARD_MESSAGE = i18n.translate(
'xpack.securitySolutionServerless.rules.endpointSecurity.agentTamperProtection.cardMessage',
{
defaultMessage: 'Upgrade your license to {productTypeRequired} to use Agent Tamper Protection.',
values: { productTypeRequired: BADGE_TEXT },
}
);
const CardDescription = styled.p`
padding: 0 33.3%;
`;
export const EndpointAgentTamperProtection = memo(() => {
return (
<EuiCard
data-test-subj="endpointPolicy-agentTamperProtectionLockedCard"
isDisabled={true}
description={false}
icon={<EuiIcon size="xl" type="lock" />}
betaBadgeProps={{
'data-test-subj': 'endpointSecurity-agentTamperProtectionLockedCard-badge',
label: BADGE_TEXT,
}}
title={
<h3 data-test-subj="endpointSecurity-agentTamperProtectionLockedCard-title">
<strong>{CARD_TITLE}</strong>
</h3>
}
>
<CardDescription>{CARD_MESSAGE}</CardDescription>
</EuiCard>
);
});
EndpointAgentTamperProtection.displayName = 'EndpointAgentTamperProtection';

View file

@ -18,3 +18,9 @@ export const RuleDetailsEndpointExceptionsLazy = lazy(() =>
default: RuleDetailsEndpointExceptions,
}))
);
export const EndpointAgentTamperProtectionLazy = lazy(() =>
import('./endpoint_agent_tamper_protection').then(({ EndpointAgentTamperProtection }) => ({
default: EndpointAgentTamperProtection,
}))
);