[Fleet] Add tamper protection toggle in agent policy settings (#157335)

## Summary
- [x] Adds a section to allow Agent tampering in Agent Policy > Settings
- [x] Link to open Uninstall command flyout is enabled when Agent
tampering is switched on
- [x] Agent tampering section is only visible if the license is platinum
or above
- [x] Unit tests


Add this to your kibana.dev.yml for testing
```
xpack.fleet.enableExperimental:
  - agentTamperProtectionEnabled
```

## Screenshots
Toggle is hidden if license is below platinum

![image](d5fe8e4c-23d5-4fd8-a3a1-3dacc61cd4c7)

Link is disabled

![image](5f9c76c9-d1da-4a29-bf99-0e93dc0449a2)

Link is enabled when switch is toggled on

![image](4000dabf-4674-4ffd-b21b-ec876ec5002f)

With Flyout opened

![image](e63a074a-bece-44e6-8504-5155190dda69)
This commit is contained in:
Candace Park 2023-05-16 18:02:53 -04:00 committed by GitHub
parent 9910615a3d
commit 3d5d1a99e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 175 additions and 2 deletions

View file

@ -0,0 +1,117 @@
/*
* 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 from 'react';
import { waitFor, fireEvent, act } from '@testing-library/react';
import type { RenderResult } from '@testing-library/react';
import { createFleetTestRendererMock } from '../../../../../../mock';
import type { TestRenderer } from '../../../../../../mock';
import { allowedExperimentalValues } from '../../../../../../../common/experimental_features';
import { ExperimentalFeaturesService } from '../../../../../../services/experimental_features';
import type { NewAgentPolicy, AgentPolicy } from '../../../../../../../common/types';
import { useLicense } from '../../../../../../hooks/use_license';
import type { LicenseService } from '../../../../../../../common/services';
import type { ValidationResults } from '../agent_policy_validation';
import { AgentPolicyAdvancedOptionsContent } from '.';
jest.mock('../../../../../../hooks/use_license');
const mockedUseLicence = useLicense as jest.MockedFunction<typeof useLicense>;
describe('Agent policy advanced options content', () => {
let testRender: TestRenderer;
let renderResult: RenderResult;
const mockAgentPolicy: Partial<NewAgentPolicy | AgentPolicy> = {
id: 'agent-policy-1',
name: 'some-agent-policy',
is_managed: false,
is_protected: false,
};
const mockUpdateAgentPolicy = jest.fn();
const mockValidation = jest.fn() as unknown as ValidationResults;
const usePlatinumLicense = () =>
mockedUseLicence.mockReturnValue({
hasAtLeast: () => true,
isPlatinum: () => true,
} as unknown as LicenseService);
const render = () => {
// remove when feature flag is removed
ExperimentalFeaturesService.init({
...allowedExperimentalValues,
agentTamperProtectionEnabled: true,
});
renderResult = testRender.render(
<AgentPolicyAdvancedOptionsContent
agentPolicy={mockAgentPolicy}
updateAgentPolicy={mockUpdateAgentPolicy}
validation={mockValidation}
/>
);
};
beforeEach(() => {
testRender = createFleetTestRendererMock();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('Agent tamper protection toggle', () => {
it('should be visible if license is at least platinum', () => {
usePlatinumLicense();
render();
expect(renderResult.queryByTestId('tamperProtectionSwitch')).toBeInTheDocument();
});
it('should not be visible if license is below platinum', () => {
mockedUseLicence.mockReturnValueOnce({
isPlatinum: () => false,
hasAtLeast: () => false,
} as unknown as LicenseService);
render();
expect(renderResult.queryByTestId('tamperProtectionSwitch')).not.toBeInTheDocument();
});
it('switched to true enables the uninstall command link', async () => {
usePlatinumLicense();
render();
await act(async () => {
fireEvent.click(renderResult.getByTestId('tamperProtectionSwitch'));
});
waitFor(() => {
expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeChecked();
expect(renderResult.getByTestId('uninstallCommandLink')).toBeEnabled();
});
});
it('switched to false disables the uninstall command link', () => {
usePlatinumLicense();
render();
expect(renderResult.getByTestId('tamperProtectionSwitch')).not.toBeChecked();
expect(renderResult.getByTestId('uninstallCommandLink')).toBeDisabled();
});
it('should update agent policy when switched on', async () => {
usePlatinumLicense();
render();
await act(async () => {
(await renderResult.findByTestId('tamperProtectionSwitch')).click();
});
expect(mockUpdateAgentPolicy).toHaveBeenCalledWith({ is_protected: true });
});
});
});

View file

@ -25,6 +25,7 @@ import {
EuiFlexItem,
EuiBetaBadge,
EuiBadge,
EuiSwitch,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
@ -35,14 +36,15 @@ import {
DEFAULT_MAX_AGENT_POLICIES_WITH_INACTIVITY_TIMEOUT,
} from '../../../../../../../common/constants';
import type { NewAgentPolicy, AgentPolicy } from '../../../../types';
import { useStartServices, useConfig, useGetAgentPolicies } from '../../../../hooks';
import { useStartServices, useConfig, useGetAgentPolicies, useLicense } from '../../../../hooks';
import { AgentPolicyPackageBadge } from '../../../../components';
import { UninstallCommandFlyout } from '../../../../../../components';
import { AgentPolicyDeleteProvider } from '../agent_policy_delete_provider';
import type { ValidationResults } from '../agent_policy_validation';
import { policyHasFleetServer } from '../../../../services';
import { ExperimentalFeaturesService, policyHasFleetServer } from '../../../../services';
import {
useOutputOptions,
@ -101,6 +103,10 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
'package_policies' in agentPolicy &&
agentPolicy?.package_policies?.some((packagePolicy) => packagePolicy.is_managed);
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
const licenseService = useLicense();
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false);
return (
<>
<EuiDescribedFormGroup
@ -119,6 +125,13 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
/>
}
>
{isUninstallCommandFlyoutOpen && (
<UninstallCommandFlyout
target="agent"
policyId={agentPolicy.id}
onClose={() => setIsUninstallCommandFlyoutOpen(false)}
/>
)}
<EuiFormRow
fullWidth
key="description"
@ -285,6 +298,48 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
}}
/>
</EuiDescribedFormGroup>
{agentTamperProtectionEnabled && licenseService.isPlatinum() && (
<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={i18n.translate('xpack.fleet.agentPolicyForm.tamperingSwitchLabel', {
defaultMessage: 'Prevent agent tampering',
})}
checked={agentPolicy.is_protected ?? false}
onChange={(e) => {
updateAgentPolicy({ is_protected: e.target.checked });
}}
data-test-subj="tamperProtectionSwitch"
/>
<EuiSpacer size="s" />
<EuiLink
onClick={() => {
setIsUninstallCommandFlyoutOpen(true);
}}
disabled={agentPolicy.is_protected === false}
data-test-subj="uninstallCommandLink"
>
{i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', {
defaultMessage: 'Get uninstall command',
})}
</EuiLink>
</EuiDescribedFormGroup>
)}
<EuiDescribedFormGroup
title={
<h4>

View file

@ -53,6 +53,7 @@ const pickAgentPolicyKeysToSend = (agentPolicy: AgentPolicy) =>
'download_source_id',
'fleet_server_host_id',
'agent_features',
'is_protected',
]);
const FormWrapper = styled.div`