From 5dd5ec218228b6ed8ae304e5d42d0eb7465ae14d Mon Sep 17 00:00:00 2001
From: Candace Park <56409205+parkiino@users.noreply.github.com>
Date: Mon, 14 Aug 2023 01:45:32 -0400
Subject: [PATCH] [Fleet][Agent Policy][Agent Tamper Protection] UI / API guard
agent tamper protection only available if security defend integration present
(#162196)
## Summary
UI
- [x] When there is no elastic defend integration present, the agent
tamper protection (`is_protected`) switch and instruction link are
disabled and there is an info tooltip explaining why the switch is
disabled
API
- [x] Requires the elastic defend integration to be present, in order to
set `is_protected` to true. Will allow the user to create the agent
policy and not throw an error, but will keep `is_protected` as false and
log a warning in the kibana server. In the next release, the response
will be modified to send back a 201 with the relevant messaging.
- [x] Sets `is_protected` to false when a user deletes the elastic
defend package policy
## Screenshots
### No Elastic Defend integration installed
---
.../common/services/agent_policies_helpers.ts | 13 +++-
.../services/agent_policy_config.test.ts | 8 +--
.../generate_new_agent_policy.test.ts | 3 +-
x-pack/plugins/fleet/common/services/index.ts | 1 +
.../index.test.tsx | 61 ++++++++++++++++---
.../agent_policy_advanced_fields/index.tsx | 34 +++++++++--
.../fleet/server/services/agent_policy.ts | 45 ++++++++++++--
.../fleet/server/services/package_policy.ts | 9 +++
.../apis/agent_policy/agent_policy.ts | 7 +--
9 files changed, 150 insertions(+), 31 deletions(-)
diff --git a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
index 52ce24634886e2..1ebe49ef4ed395 100644
--- a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
+++ b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
@@ -5,8 +5,13 @@
* 2.0.
*/
-import type { AgentPolicy } from '../types';
-import { FLEET_SERVER_PACKAGE, FLEET_APM_PACKAGE, FLEET_SYNTHETICS_PACKAGE } from '../constants';
+import type { NewAgentPolicy, AgentPolicy } from '../types';
+import {
+ FLEET_SERVER_PACKAGE,
+ FLEET_APM_PACKAGE,
+ FLEET_SYNTHETICS_PACKAGE,
+ FLEET_ENDPOINT_PACKAGE,
+} from '../constants';
export function policyHasFleetServer(agentPolicy: AgentPolicy) {
if (!agentPolicy.package_policies) {
@@ -26,6 +31,10 @@ export function policyHasSyntheticsIntegration(agentPolicy: AgentPolicy) {
return policyHasIntegration(agentPolicy, FLEET_SYNTHETICS_PACKAGE);
}
+export function policyHasEndpointSecurity(agentPolicy: Partial) {
+ return policyHasIntegration(agentPolicy as AgentPolicy, FLEET_ENDPOINT_PACKAGE);
+}
+
function policyHasIntegration(agentPolicy: AgentPolicy, packageName: string) {
if (!agentPolicy.package_policies) {
return false;
diff --git a/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts b/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts
index 70ee2ae631a3cc..e9bd5aa23b5d9f 100644
--- a/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts
+++ b/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts
@@ -5,15 +5,15 @@
* 2.0.
*/
+import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import { pick } from 'lodash';
-import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
+import { createAgentPolicyMock } from '../mocks';
import {
isAgentPolicyValidForLicense,
unsetAgentPolicyAccordingToLicenseLevel,
} from './agent_policy_config';
-import { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy';
describe('agent policy config and licenses', () => {
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
@@ -34,13 +34,13 @@ describe('agent policy config and licenses', () => {
});
describe('unsetAgentPolicyAccordingToLicenseLevel', () => {
it('resets all paid features to default if license is gold', () => {
- const defaults = pick(generateNewAgentPolicyWithDefaults(), 'is_protected');
+ const defaults = pick(createAgentPolicyMock(), 'is_protected');
const partialPolicy = { is_protected: true };
const retPolicy = unsetAgentPolicyAccordingToLicenseLevel(partialPolicy, Gold);
expect(retPolicy).toEqual(defaults);
});
it('does not change paid features if license is platinum', () => {
- const expected = pick(generateNewAgentPolicyWithDefaults(), 'is_protected');
+ const expected = pick(createAgentPolicyMock(), 'is_protected');
const partialPolicy = { is_protected: false };
const expected2 = { is_protected: true };
const partialPolicy2 = { is_protected: true };
diff --git a/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts b/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts
index 97e63be4bd7013..bc4a6b55f75ee8 100644
--- a/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts
+++ b/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts
@@ -27,7 +27,6 @@ describe('generateNewAgentPolicyWithDefaults', () => {
description: 'test description',
namespace: 'test-namespace',
monitoring_enabled: ['logs'],
- is_protected: true,
});
expect(newAgentPolicy).toEqual({
@@ -36,7 +35,7 @@ describe('generateNewAgentPolicyWithDefaults', () => {
namespace: 'test-namespace',
monitoring_enabled: ['logs'],
inactivity_timeout: 1209600,
- is_protected: true,
+ is_protected: false,
});
});
});
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index abf06ac54b07cf..04f74404ba382e 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -66,6 +66,7 @@ export { agentStatusesToSummary } from './agent_statuses_to_summary';
export {
policyHasFleetServer,
policyHasAPMIntegration,
+ policyHasEndpointSecurity,
policyHasSyntheticsIntegration,
} from './agent_policies_helpers';
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx
index 019395c0cb5f60..fe21159ab347b3 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx
@@ -17,11 +17,13 @@ import { allowedExperimentalValues } from '../../../../../../../common/experimen
import { ExperimentalFeaturesService } from '../../../../../../services/experimental_features';
-import type { NewAgentPolicy, AgentPolicy } from '../../../../../../../common/types';
+import { createAgentPolicyMock, createPackagePolicyMock } from '../../../../../../../common/mocks';
+import type { AgentPolicy, NewAgentPolicy } from '../../../../../../../common/types';
import { useLicense } from '../../../../../../hooks/use_license';
import type { LicenseService } from '../../../../../../../common/services';
+import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services';
import type { ValidationResults } from '../agent_policy_validation';
@@ -34,12 +36,7 @@ const mockedUseLicence = useLicense as jest.MockedFunction;
describe('Agent policy advanced options content', () => {
let testRender: TestRenderer;
let renderResult: RenderResult;
-
- const mockAgentPolicy: Partial = {
- name: 'some-agent-policy',
- is_managed: false,
- };
-
+ let mockAgentPolicy: Partial;
const mockUpdateAgentPolicy = jest.fn();
const mockValidation = jest.fn() as unknown as ValidationResults;
const usePlatinumLicense = () =>
@@ -48,16 +45,34 @@ describe('Agent policy advanced options content', () => {
isPlatinum: () => true,
} as unknown as LicenseService);
- const render = ({ isProtected = false, policyId = 'agent-policy-1' } = {}) => {
+ const render = ({
+ isProtected = false,
+ policyId = 'agent-policy-1',
+ newAgentPolicy = false,
+ packagePolicy = [createPackagePolicyMock()],
+ } = {}) => {
// remove when feature flag is removed
ExperimentalFeaturesService.init({
...allowedExperimentalValues,
agentTamperProtectionEnabled: true,
});
+ if (newAgentPolicy) {
+ mockAgentPolicy = generateNewAgentPolicyWithDefaults();
+ } else {
+ mockAgentPolicy = {
+ ...createAgentPolicyMock(),
+ package_policies: packagePolicy,
+ id: policyId,
+ };
+ }
+
renderResult = testRender.render(
@@ -118,5 +133,33 @@ describe('Agent policy advanced options content', () => {
});
expect(mockUpdateAgentPolicy).toHaveBeenCalledWith({ is_protected: true });
});
+ describe('when the defend integration is not installed', () => {
+ beforeEach(() => {
+ usePlatinumLicense();
+ render({
+ packagePolicy: [
+ {
+ ...createPackagePolicyMock(),
+ package: { name: 'not-endpoint', title: 'Not Endpoint', version: '0.1.0' },
+ },
+ ],
+ isProtected: true,
+ });
+ });
+ it('should disable the switch and uninstall command link', () => {
+ expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeDisabled();
+ expect(renderResult.getByTestId('uninstallCommandLink')).toBeDisabled();
+ });
+ it('should show an icon tip explaining why the switch is disabled', () => {
+ expect(renderResult.getByTestId('tamperMissingIntegrationTooltip')).toBeTruthy();
+ });
+ });
+ describe('when the user is creating a new agent policy', () => {
+ it('should be disabled, since it has no package policies and therefore elastic defend integration is not installed', async () => {
+ usePlatinumLicense();
+ render({ newAgentPolicy: true });
+ expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeDisabled();
+ });
+ });
});
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
index 8811a7d97ed610..49288da22c9350 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import {
EuiDescribedFormGroup,
EuiFormRow,
@@ -46,6 +46,8 @@ import type { ValidationResults } from '../agent_policy_validation';
import { ExperimentalFeaturesService, policyHasFleetServer } from '../../../../services';
+import { policyHasEndpointSecurity as hasElasticDefend } from '../../../../../../../common/services';
+
import {
useOutputOptions,
useDownloadSourcesOptions,
@@ -106,6 +108,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
const licenseService = useLicense();
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false);
+ const policyHasElasticDefend = useMemo(() => hasElasticDefend(agentPolicy), [agentPolicy]);
return (
<>
@@ -317,13 +320,34 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
}
>
+ {' '}
+ {!policyHasElasticDefend && (
+
+
+
+ )}
+ >
+ }
checked={agentPolicy.is_protected ?? false}
onChange={(e) => {
updateAgentPolicy({ is_protected: e.target.checked });
}}
+ disabled={!policyHasElasticDefend}
data-test-subj="tamperProtectionSwitch"
/>
{agentPolicy.id && (
@@ -333,7 +357,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
onClick={() => {
setIsUninstallCommandFlyoutOpen(true);
}}
- disabled={agentPolicy.is_protected === false}
+ disabled={!agentPolicy.is_protected || !policyHasElasticDefend}
data-test-subj="uninstallCommandLink"
>
{i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', {
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 6958ea80c00d60..44635eee45200a 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -22,6 +22,8 @@ import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/typesWithB
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
+import { policyHasEndpointSecurity } from '../../common/services';
+
import { populateAssignedAgentsCount } from '../routes/agent_policy/handlers';
import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
@@ -113,7 +115,10 @@ class AgentPolicyService {
id: string,
agentPolicy: Partial,
user?: AuthenticatedUser,
- options: { bumpRevision: boolean } = { bumpRevision: true }
+ options: { bumpRevision: boolean; removeProtection: boolean } = {
+ bumpRevision: true,
+ removeProtection: false,
+ }
): Promise {
auditLoggingService.writeCustomSoAuditLog({
action: 'update',
@@ -136,6 +141,12 @@ class AgentPolicyService {
);
}
+ const logger = appContextService.getLogger();
+
+ if (options.removeProtection) {
+ logger.warn(`Setting tamper protection for Agent Policy ${id} to false`);
+ }
+
await validateOutputForPolicy(
soClient,
agentPolicy,
@@ -145,11 +156,14 @@ class AgentPolicyService {
await soClient.update(SAVED_OBJECT_TYPE, id, {
...agentPolicy,
...(options.bumpRevision ? { revision: existingAgentPolicy.revision + 1 } : {}),
+ ...(options.removeProtection
+ ? { is_protected: false }
+ : { is_protected: agentPolicy.is_protected }),
updated_at: new Date().toISOString(),
updated_by: user ? user.username : 'system',
});
- if (options.bumpRevision) {
+ if (options.bumpRevision || options.removeProtection) {
await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', id);
}
@@ -239,6 +253,14 @@ class AgentPolicyService {
this.checkTamperProtectionLicense(agentPolicy);
+ const logger = appContextService.getLogger();
+
+ if (agentPolicy?.is_protected) {
+ logger.warn(
+ 'Agent policy requires Elastic Defend integration to set tamper protection to true'
+ );
+ }
+
await this.requireUniqueName(soClient, agentPolicy);
await validateOutputForPolicy(soClient, agentPolicy);
@@ -253,7 +275,7 @@ class AgentPolicyService {
updated_at: new Date().toISOString(),
updated_by: options?.user?.username || 'system',
schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION,
- is_protected: agentPolicy.is_protected ?? false,
+ is_protected: false,
} as AgentPolicy,
options
);
@@ -491,6 +513,16 @@ class AgentPolicyService {
this.checkTamperProtectionLicense(agentPolicy);
+ const logger = appContextService.getLogger();
+
+ if (agentPolicy?.is_protected && !policyHasEndpointSecurity(existingAgentPolicy)) {
+ logger.warn(
+ 'Agent policy requires Elastic Defend integration to set tamper protection to true'
+ );
+ // force agent policy to be false if elastic defend is not present
+ agentPolicy.is_protected = false;
+ }
+
if (existingAgentPolicy.is_managed && !options?.force) {
Object.entries(agentPolicy)
.filter(([key]) => !KEY_EDITABLE_FOR_MANAGED_POLICIES.includes(key))
@@ -586,9 +618,12 @@ class AgentPolicyService {
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string,
- options?: { user?: AuthenticatedUser }
+ options?: { user?: AuthenticatedUser; removeProtection?: boolean }
): Promise {
- const res = await this._update(soClient, esClient, id, {}, options?.user);
+ const res = await this._update(soClient, esClient, id, {}, options?.user, {
+ bumpRevision: true,
+ removeProtection: options?.removeProtection ?? false,
+ });
return res;
}
diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts
index 72fddd0e5b6747..b64ae2a2a1c42b 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.ts
@@ -1161,13 +1161,22 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
...new Set(result.filter((r) => r.success && r.policy_id).map((r) => r.policy_id!)),
];
+ const agentPoliciesWithEndpointPackagePolicies = result.reduce((acc, cur) => {
+ if (cur.success && cur.policy_id && cur.package?.name === 'endpoint') {
+ return acc.add(cur.policy_id);
+ }
+ return acc;
+ }, new Set());
+
const agentPolicies = await agentPolicyService.getByIDs(soClient, uniquePolicyIdsR);
for (const policyId of uniquePolicyIdsR) {
const agentPolicy = agentPolicies.find((p) => p.id === policyId);
if (agentPolicy) {
+ // is the agent policy attached to package policy with endpoint
await agentPolicyService.bumpRevision(soClient, esClient, policyId, {
user: options?.user,
+ removeProtection: agentPoliciesWithEndpointPackagePolicies.has(policyId),
});
}
}
diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
index 47d3f93fd8ddf5..90426d9bdfa3ed 100644
--- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
+++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
@@ -119,7 +119,6 @@ export default function (providerContext: FtrProviderContext) {
expect(body.item.is_managed).to.equal(false);
expect(body.item.inactivity_timeout).to.equal(1209600);
expect(body.item.status).to.be('active');
- expect(body.item.is_protected).to.equal(false);
});
it('sets given is_managed value', async () => {
@@ -445,13 +444,13 @@ export default function (providerContext: FtrProviderContext) {
status: 'active',
description: 'Test',
is_managed: false,
- is_protected: false,
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
revision: 1,
schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION,
updated_by: 'elastic',
package_policies: [],
+ is_protected: false,
});
});
@@ -732,7 +731,7 @@ export default function (providerContext: FtrProviderContext) {
name: 'Updated name',
description: 'Updated description',
namespace: 'default',
- is_protected: true,
+ is_protected: false,
})
.expect(200);
createdPolicyIds.push(updatedPolicy.id);
@@ -750,7 +749,7 @@ export default function (providerContext: FtrProviderContext) {
updated_by: 'elastic',
inactivity_timeout: 1209600,
package_policies: [],
- is_protected: true,
+ is_protected: false,
});
});