Skip to content

Commit

Permalink
[Fleet] Add warning if need root integrations trying to be used with …
Browse files Browse the repository at this point in the history
…unprivileged agents (elastic#183283)

## Summary

Closes elastic/ingest-dev#3252

## Add integration
Added warning to Add integration when the integration requires root
privilege and the selected existing agent policy has unprivileged agents
enrolled.

To verify:
- enroll an agent with docker (it has unprivileged: true)
- try to add an integration that requires root e.g. auditd_manager
- verify that when trying to save the integration, the warning callout
is part of the confirm deploy modal

<img width="807" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/420da729-a4f4-4861-9767-001699629397">

## Add agent flyout
Added warning to Add agent flyout when an unprivileged agent is detected
in combination with an agent policy that has integrations requiring root

To verify:
- add an integration to an agent policy that requires root e.g.
auditd_manager
- open Add agent flyout, verify that the warning callout is visible

<img width="1273" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/e4ae1d73-358b-4d3c-9ca0-27e88bc734a6">

### Open question:
- Do we want to show the warning on `Add agent flyout` only for newly
enrolled agents (in the last 10 mins like we query enrolled agents), or
any unprivileged agents that are enrolled to this policy?
- Decision: No longer applicable as we decided to not show a count here

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
juliaElastic and kibanamachine authored May 21, 2024
1 parent 130bf7b commit ad03dfb
Show file tree
Hide file tree
Showing 38 changed files with 738 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export const HASH_TO_VERSION_MAP = {
'ingest-agent-policies|0fd93cd11c019b118e93a9157c22057b': '10.1.0',
'ingest-download-sources|0b0f6828e59805bd07a650d80817c342': '10.0.0',
'ingest-outputs|b1237f7fdc0967709e75d65d208ace05': '10.6.0',
'ingest-package-policies|a1a074bad36e68d54f98d2158d60f879': '10.0.0',
'ingest-package-policies|aef7977b81f7930f23cbfd8711ba272e': '10.9.0',
'inventory-view|3d1b76c39bfb2cc8296b024d73854724': '10.0.0',
'kql-telemetry|3d1b76c39bfb2cc8296b024d73854724': '10.0.0',
'legacy-url-alias|0750774cf16475f88f2361e99cc5c8f0': '8.2.0',
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-check-mappings-update-cli/current_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@
"overrides",
"package",
"package.name",
"package.requires_root",
"package.title",
"package.version",
"policy_id",
Expand Down
3 changes: 3 additions & 0 deletions packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2017,6 +2017,9 @@
"name": {
"type": "keyword"
},
"requires_root": {
"type": "boolean"
},
"title": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-agent-policies": "803dc27e106440c41e8f3c3d8ee8bbb0821bcde2",
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91",
"ingest-package-policies": "e6da7d0ee2996241ade23b3a7811fe5d3e449cb2",
"ingest-package-policies": "44c682a6bf23993c665f0a60a427f3c120a0a10d",
"ingest_manager_settings": "91445219e7115ff0c45d1dabd5d614a80b421797",
"inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83",
"kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad",
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -7446,6 +7446,9 @@
},
"title": {
"type": "string"
},
"requires_root": {
"type": "boolean"
}
},
"required": [
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4776,6 +4776,8 @@ components:
type: string
title:
type: string
requires_root:
type: boolean
required:
- name
- version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ properties:
agents:
type: number
unprivileged_agents:
type: number
type: number
agent_features:
type: array
items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ properties:
type: string
title:
type: string
requires_root:
type: boolean
required:
- name
- version
Expand Down
43 changes: 42 additions & 1 deletion x-pack/plugins/fleet/common/services/package_helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { isRootPrivilegesRequired } from './package_helpers';
import { getRootIntegrations, isRootPrivilegesRequired } from './package_helpers';

describe('isRootPrivilegesRequired', () => {
it('should return true if root privileges is required at root level', () => {
Expand Down Expand Up @@ -38,3 +38,44 @@ describe('isRootPrivilegesRequired', () => {
expect(res).toBe(false);
});
});

describe('getRootIntegrations', () => {
it('should return packages that require root', () => {
const res = getRootIntegrations([
{
package: {
requires_root: true,
name: 'auditd_manager',
title: 'Auditd Manager',
},
} as any,
{
package: {
requires_root: false,
name: 'system',
title: 'System',
},
} as any,
{
package: {
name: 'test',
title: 'Test',
},
} as any,
{
package: {
requires_root: true,
name: 'auditd_manager',
title: 'Auditd Manager',
},
} as any,
{} as any,
]);
expect(res).toEqual([{ name: 'auditd_manager', title: 'Auditd Manager' }]);
});

it('should return empty array if no packages require root', () => {
const res = getRootIntegrations([]);
expect(res).toEqual([]);
});
});
15 changes: 14 additions & 1 deletion x-pack/plugins/fleet/common/services/package_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* 2.0.
*/

import type { PackageInfo } from '../types';
import { uniqBy } from 'lodash';

import type { PackageInfo, PackagePolicy } from '../types';

/**
* Return true if a package need Elastic Agent to be run as root/administrator
Expand All @@ -16,3 +18,14 @@ export function isRootPrivilegesRequired(packageInfo: PackageInfo) {
packageInfo.data_streams?.some((d) => d.agent?.privileges?.root)
);
}

export function getRootIntegrations(
packagePolicies: PackagePolicy[]
): Array<{ name: string; title: string }> {
return uniqBy(
packagePolicies
.map((policy) => policy.package)
.filter((pkg) => (pkg && pkg.requires_root) || false),
(pkg) => pkg!.name
).map((pkg) => ({ name: pkg!.name, title: pkg!.title }));
}
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/types/models/package_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface PackagePolicyPackage {
title: string;
version: string;
experimental_data_stream_features?: ExperimentalDataStreamFeature[];
requires_root?: boolean;
}

export interface PackagePolicyConfigRecordEntry {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';

import type { AgentPolicy } from '../../../types';
import { UnprivilegedAgentsCallout } from '../create_package_policy_page/single_page_layout/confirm_modal';

export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
onConfirm: () => void;
onCancel: () => void;
agentCount: number;
agentPolicy: AgentPolicy;
}> = ({ onConfirm, onCancel, agentCount, agentPolicy }) => {
showUnprivilegedAgentsCallout?: boolean;
unprivilegedAgentsCount?: number;
}> = ({
onConfirm,
onCancel,
agentCount,
agentPolicy,
showUnprivilegedAgentsCallout = false,
unprivilegedAgentsCount = 0,
}) => {
return (
<EuiConfirmModal
title={
Expand Down Expand Up @@ -64,6 +74,15 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
/>
</div>
</EuiCallOut>
{showUnprivilegedAgentsCallout && (
<>
<EuiSpacer size="m" />
<UnprivilegedAgentsCallout
agentPolicyName={agentPolicy.name}
unprivilegedAgentsCount={unprivilegedAgentsCount}
/>
</>
)}
<EuiSpacer size="l" />
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalDescription"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const InstallElasticAgentPageStep: React.FC<MultiPageStepLayoutProps> = (
const [localIsManaged, setLocalIsManaged] = useState(props.isManaged);
const [useLocalState, setUseLocalState] = useState(false);

const enrolledAgentIds = usePollingAgentCount(props.agentPolicy?.id || '', {
const { enrolledAgentIds } = usePollingAgentCount(props.agentPolicy?.id || '', {
noLowerTimeLimit: true,
pollImmediately: true,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { ManualInstructions } from '../../../../../../../../../components/enroll

import { KubernetesManifestApplyStep } from '../../../../../../../../../components/agent_enrollment_flyout/steps/run_k8s_apply_command_step';

import { getRootIntegrations } from '../../../../../../../../../../common/services';

import type { InstallAgentPageProps } from './types';

export const InstallElasticAgentManagedPageStep: React.FC<InstallAgentPageProps> = (props) => {
Expand Down Expand Up @@ -80,6 +82,7 @@ export const InstallElasticAgentManagedPageStep: React.FC<InstallAgentPageProps>
fullCopyButton: true,
fleetServerHost,
onCopy: () => setCommandCopied(true),
rootIntegrations: getRootIntegrations(agentPolicy?.package_policies ?? []),
}),
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { safeDump } from 'js-yaml';

import type { FullAgentPolicy } from '../../../../../../../../../../common/types/models/agent_policy';
import { API_VERSIONS } from '../../../../../../../../../../common/constants';
import { getRootIntegrations } from '../../../../../../../../../../common/services';
import {
AgentStandaloneBottomBar,
StandaloneModeWarningCallout,
Expand Down Expand Up @@ -112,6 +113,7 @@ export const InstallElasticAgentStandalonePageStep: React.FC<InstallAgentPagePro
isComplete: yaml && commandCopied,
fullCopyButton: true,
onCopy: () => setCommandCopied(true),
rootIntegrations: getRootIntegrations(agentPolicy?.package_policies ?? []),
}),
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 { EuiCallOut, EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';

export interface UnprivilegedConfirmModalProps {
onConfirm: () => void;
onCancel: () => void;
agentPolicyName: string;
unprivilegedAgentsCount: number;
}

export const UnprivilegedConfirmModal: React.FC<UnprivilegedConfirmModalProps> = ({
onConfirm,
onCancel,
agentPolicyName,
unprivilegedAgentsCount,
}: UnprivilegedConfirmModalProps) => {
return (
<EuiConfirmModal
title={
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModalTitle"
defaultMessage="Confirm add integration"
/>
}
onCancel={onCancel}
onConfirm={onConfirm}
cancelButtonText={
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
}
confirmButtonText={
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.confirmButtonLabel"
defaultMessage="Add integration"
/>
}
buttonColor="warning"
>
<UnprivilegedAgentsCallout
unprivilegedAgentsCount={unprivilegedAgentsCount}
agentPolicyName={agentPolicyName}
/>
</EuiConfirmModal>
);
};

export const UnprivilegedAgentsCallout: React.FC<{
agentPolicyName: string;
unprivilegedAgentsCount: number;
}> = ({ agentPolicyName, unprivilegedAgentsCount }) => {
return (
<EuiCallOut
color="warning"
iconType="warning"
title={i18n.translate('xpack.fleet.addIntegration.confirmModal.unprivilegedAgentsTitle', {
defaultMessage: 'Unprivileged agents enrolled to the selected policy',
})}
data-test-subj="unprivilegedAgentsCallout"
>
<FormattedMessage
id="xpack.fleet.addIntegration.confirmModal.unprivilegedAgentsMessage"
defaultMessage="This integration requires Elastic Agents to have root privileges. There {unprivilegedAgentsCount, plural, one {is # agent} other {are # agents}} running in an unprivileged mode using {agentPolicyName}. To ensure that all data required by the integration can be collected, re-enroll the {unprivilegedAgentsCount, plural, one {agent} other {agents}} using an account with root privileges."
values={{
unprivilegedAgentsCount,
agentPolicyName,
}}
/>
</EuiCallOut>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import {
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
SO_SEARCH_LIMIT,
} from '../../../../../../../../common';
import { getMaxPackageName } from '../../../../../../../../common/services';
import {
getMaxPackageName,
isRootPrivilegesRequired,
} from '../../../../../../../../common/services';
import { useConfirmForceInstall } from '../../../../../../integrations/hooks';
import { validatePackagePolicy, validationHasErrors } from '../../services';
import type { PackagePolicyValidationResults } from '../../services';
Expand Down Expand Up @@ -266,6 +269,16 @@ export function useOnSubmit({
setFormState('CONFIRM');
return;
}
if (
packageInfo &&
isRootPrivilegesRequired(packageInfo) &&
(agentPolicy?.unprivileged_agents ?? 0) > 0 &&
formState !== 'CONFIRM' &&
formState !== 'CONFIRM_UNPRIVILEGED'
) {
setFormState('CONFIRM_UNPRIVILEGED');
return;
}
let createdPolicy = overrideCreatedAgentPolicy;
if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
try {
Expand Down
Loading

0 comments on commit ad03dfb

Please sign in to comment.