mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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
<img width="970" alt="image"
src="910be766
-1a1e-4580-9ace-306089b4626d">
This commit is contained in:
parent
4802b0dbfe
commit
5dd5ec2182
9 changed files with 150 additions and 31 deletions
|
@ -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<NewAgentPolicy | AgentPolicy>) {
|
||||
return policyHasIntegration(agentPolicy as AgentPolicy, FLEET_ENDPOINT_PACKAGE);
|
||||
}
|
||||
|
||||
function policyHasIntegration(agentPolicy: AgentPolicy, packageName: string) {
|
||||
if (!agentPolicy.package_policies) {
|
||||
return false;
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -66,6 +66,7 @@ export { agentStatusesToSummary } from './agent_statuses_to_summary';
|
|||
export {
|
||||
policyHasFleetServer,
|
||||
policyHasAPMIntegration,
|
||||
policyHasEndpointSecurity,
|
||||
policyHasSyntheticsIntegration,
|
||||
} from './agent_policies_helpers';
|
||||
|
||||
|
|
|
@ -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<typeof useLicense>;
|
|||
describe('Agent policy advanced options content', () => {
|
||||
let testRender: TestRenderer;
|
||||
let renderResult: RenderResult;
|
||||
|
||||
const mockAgentPolicy: Partial<NewAgentPolicy | AgentPolicy> = {
|
||||
name: 'some-agent-policy',
|
||||
is_managed: false,
|
||||
};
|
||||
|
||||
let mockAgentPolicy: Partial<NewAgentPolicy | AgentPolicy>;
|
||||
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(
|
||||
<AgentPolicyAdvancedOptionsContent
|
||||
agentPolicy={{ ...mockAgentPolicy, is_protected: isProtected, id: policyId }}
|
||||
agentPolicy={{
|
||||
...mockAgentPolicy,
|
||||
is_protected: isProtected,
|
||||
}}
|
||||
updateAgentPolicy={mockUpdateAgentPolicy}
|
||||
validation={mockValidation}
|
||||
/>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Props> =
|
|||
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<Props> =
|
|||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.fleet.agentPolicyForm.tamperingSwitchLabel', {
|
||||
defaultMessage: 'Prevent agent tampering',
|
||||
})}
|
||||
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 && (
|
||||
|
@ -333,7 +357,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent<Props> =
|
|||
onClick={() => {
|
||||
setIsUninstallCommandFlyoutOpen(true);
|
||||
}}
|
||||
disabled={agentPolicy.is_protected === false}
|
||||
disabled={!agentPolicy.is_protected || !policyHasElasticDefend}
|
||||
data-test-subj="uninstallCommandLink"
|
||||
>
|
||||
{i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', {
|
||||
|
|
|
@ -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<AgentPolicySOAttributes>,
|
||||
user?: AuthenticatedUser,
|
||||
options: { bumpRevision: boolean } = { bumpRevision: true }
|
||||
options: { bumpRevision: boolean; removeProtection: boolean } = {
|
||||
bumpRevision: true,
|
||||
removeProtection: false,
|
||||
}
|
||||
): Promise<AgentPolicy> {
|
||||
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<AgentPolicySOAttributes>(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<AgentPolicy> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue