mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Add warning if need root integrations trying to be used with unprivileged agents (#183283)
## Summary Closes https://github.com/elastic/ingest-dev/issues/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="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="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 <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
130bf7b9c5
commit
ad03dfb1f5
38 changed files with 738 additions and 40 deletions
|
@ -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',
|
||||
|
|
|
@ -602,6 +602,7 @@
|
|||
"overrides",
|
||||
"package",
|
||||
"package.name",
|
||||
"package.requires_root",
|
||||
"package.title",
|
||||
"package.version",
|
||||
"policy_id",
|
||||
|
|
|
@ -2017,6 +2017,9 @@
|
|||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"requires_root": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"type": "keyword"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -7446,6 +7446,9 @@
|
|||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"requires_root": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
@ -4776,6 +4776,8 @@ components:
|
|||
type: string
|
||||
title:
|
||||
type: string
|
||||
requires_root:
|
||||
type: boolean
|
||||
required:
|
||||
- name
|
||||
- version
|
||||
|
|
|
@ -51,7 +51,7 @@ properties:
|
|||
agents:
|
||||
type: number
|
||||
unprivileged_agents:
|
||||
type: number
|
||||
type: number
|
||||
agent_features:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -13,6 +13,8 @@ properties:
|
|||
type: string
|
||||
title:
|
||||
type: string
|
||||
requires_root:
|
||||
type: boolean
|
||||
required:
|
||||
- name
|
||||
- version
|
||||
|
|
|
@ -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', () => {
|
||||
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
@ -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 }));
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface PackagePolicyPackage {
|
|||
title: string;
|
||||
version: string;
|
||||
experimental_data_stream_features?: ExperimentalDataStreamFeature[];
|
||||
requires_root?: boolean;
|
||||
}
|
||||
|
||||
export interface PackagePolicyConfigRecordEntry {
|
||||
|
|
|
@ -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={
|
||||
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
@ -80,6 +82,7 @@ export const InstallElasticAgentManagedPageStep: React.FC<InstallAgentPageProps>
|
|||
fullCopyButton: true,
|
||||
fleetServerHost,
|
||||
onCopy: () => setCommandCopied(true),
|
||||
rootIntegrations: getRootIntegrations(agentPolicy?.package_policies ?? []),
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -112,6 +113,7 @@ export const InstallElasticAgentStandalonePageStep: React.FC<InstallAgentPagePro
|
|||
isComplete: yaml && commandCopied,
|
||||
fullCopyButton: true,
|
||||
onCopy: () => setCommandCopied(true),
|
||||
rootIntegrations: getRootIntegrations(agentPolicy?.package_policies ?? []),
|
||||
}),
|
||||
];
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
|
|
@ -37,14 +37,28 @@ jest.mock('../../../../hooks', () => {
|
|||
sendGetAgentStatus: jest.fn().mockResolvedValue({ data: { results: { total: 0 } } }),
|
||||
useGetAgentPolicies: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }],
|
||||
items: [
|
||||
{
|
||||
id: 'agent-policy-1',
|
||||
name: 'Agent policy 1',
|
||||
namespace: 'default',
|
||||
unprivileged_agents: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
error: undefined,
|
||||
isLoading: false,
|
||||
resendRequest: jest.fn(),
|
||||
} as any),
|
||||
sendGetOneAgentPolicy: jest.fn().mockResolvedValue({
|
||||
data: { item: { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' } },
|
||||
data: {
|
||||
item: {
|
||||
id: 'agent-policy-1',
|
||||
name: 'Agent policy 1',
|
||||
namespace: 'default',
|
||||
unprivileged_agents: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
useGetPackageInfoByKeyQuery: jest.fn(),
|
||||
sendGetSettings: jest.fn().mockResolvedValue({
|
||||
|
@ -129,16 +143,9 @@ describe('when on the package policy create page', () => {
|
|||
/>
|
||||
</Route>
|
||||
));
|
||||
let mockPackageInfo: any;
|
||||
|
||||
beforeEach(() => {
|
||||
testRenderer = createFleetTestRendererMock();
|
||||
mockApiCalls(testRenderer.startServices.http);
|
||||
testRenderer.mountHistory.push(createPageUrlPath);
|
||||
|
||||
jest.mocked(useStartServices().application.navigateToApp).mockReset();
|
||||
|
||||
mockPackageInfo = {
|
||||
function getMockPackageInfo(requiresRoot?: boolean) {
|
||||
return {
|
||||
data: {
|
||||
item: {
|
||||
name: 'nginx',
|
||||
|
@ -193,12 +200,25 @@ describe('when on the package policy create page', () => {
|
|||
latestVersion: '1.3.0',
|
||||
keepPoliciesUpToDate: false,
|
||||
status: 'not_installed',
|
||||
agent: {
|
||||
privileges: {
|
||||
root: requiresRoot,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(mockPackageInfo);
|
||||
beforeEach(() => {
|
||||
testRenderer = createFleetTestRendererMock();
|
||||
mockApiCalls(testRenderer.startServices.http);
|
||||
testRenderer.mountHistory.push(createPageUrlPath);
|
||||
|
||||
jest.mocked(useStartServices().application.navigateToApp).mockReset();
|
||||
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(getMockPackageInfo());
|
||||
});
|
||||
|
||||
describe('and Route state is provided via Fleet HashRouter', () => {
|
||||
|
@ -280,6 +300,73 @@ describe('when on the package policy create page', () => {
|
|||
vars: undefined,
|
||||
};
|
||||
|
||||
test('should show unprivileged warning modal on submit if conditions match', async () => {
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(getMockPackageInfo(true));
|
||||
await act(async () => {
|
||||
render('agent-policy-1');
|
||||
});
|
||||
|
||||
let saveBtn: HTMLElement;
|
||||
|
||||
await waitFor(() => {
|
||||
saveBtn = renderResult.getByText(/Save and continue/).closest('button')!;
|
||||
expect(saveBtn).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveBtn);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
renderResult.getByText('Unprivileged agents enrolled to the selected policy')
|
||||
).toBeInTheDocument();
|
||||
expect(renderResult.getByTestId('unprivilegedAgentsCallout').textContent).toContain(
|
||||
'This integration requires Elastic Agents to have root privileges. There is 1 agent running in an unprivileged mode using Agent policy 1. To ensure that all data required by the integration can be collected, re-enroll the agent using an account with root privileges.'
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
saveBtn = renderResult.getByText(/Add integration/).closest('button')!;
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(saveBtn);
|
||||
});
|
||||
|
||||
expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should show unprivileged warning and agents modal on submit if conditions match', async () => {
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(getMockPackageInfo(true));
|
||||
(sendGetAgentStatus as jest.MockedFunction<any>).mockResolvedValueOnce({
|
||||
data: { results: { total: 1 } },
|
||||
});
|
||||
await act(async () => {
|
||||
render('agent-policy-1');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.getByText('This action will update 1 agent')).toBeInTheDocument();
|
||||
expect(
|
||||
renderResult.getByText('Unprivileged agents enrolled to the selected policy')
|
||||
).toBeInTheDocument();
|
||||
expect(renderResult.getByTestId('unprivilegedAgentsCallout').textContent).toContain(
|
||||
'This integration requires Elastic Agents to have root privileges. There is 1 agent running in an unprivileged mode using Agent policy 1. To ensure that all data required by the integration can be collected, re-enroll the agent using an account with root privileges.'
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
|
||||
});
|
||||
|
||||
expect(sendCreatePackagePolicy as jest.MockedFunction<any>).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should create package policy on submit when query param agent policy id is set', async () => {
|
||||
await act(async () => {
|
||||
render('agent-policy-1');
|
||||
|
@ -384,10 +471,10 @@ describe('when on the package policy create page', () => {
|
|||
|
||||
test('should create agent policy without sys monitoring when new hosts is selected for system integration', async () => {
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue({
|
||||
...mockPackageInfo,
|
||||
...getMockPackageInfo(),
|
||||
data: {
|
||||
item: {
|
||||
...mockPackageInfo.data!.item,
|
||||
...getMockPackageInfo().data!.item,
|
||||
name: 'system',
|
||||
},
|
||||
},
|
||||
|
@ -472,7 +559,7 @@ describe('when on the package policy create page', () => {
|
|||
expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should not show modal if agent policy has agents', async () => {
|
||||
test('should show modal if agent policy has agents', async () => {
|
||||
(sendGetAgentStatus as jest.MockedFunction<any>).mockResolvedValueOnce({
|
||||
data: { results: { total: 1 } },
|
||||
});
|
||||
|
|
|
@ -75,6 +75,7 @@ import { useDevToolsRequest, useOnSubmit, useSetupTechnology } from './hooks';
|
|||
import { PostInstallCloudFormationModal } from './components/cloud_security_posture/post_install_cloud_formation_modal';
|
||||
import { PostInstallGoogleCloudShellModal } from './components/cloud_security_posture/post_install_google_cloud_shell_modal';
|
||||
import { PostInstallAzureArmTemplateModal } from './components/cloud_security_posture/post_install_azure_arm_template_modal';
|
||||
import { UnprivilegedConfirmModal } from './confirm_modal';
|
||||
|
||||
const StepsWithLessPadding = styled(EuiSteps)`
|
||||
.euiStep__content {
|
||||
|
@ -436,6 +437,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CreatePackagePolicySinglePageLayout {...layoutProps} data-test-subj="createPackagePolicy">
|
||||
<EuiErrorBoundary>
|
||||
|
@ -445,8 +447,22 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
|
|||
agentPolicy={agentPolicy}
|
||||
onConfirm={onSubmit}
|
||||
onCancel={() => setFormState('VALID')}
|
||||
showUnprivilegedAgentsCallout={Boolean(
|
||||
packageInfo &&
|
||||
isRootPrivilegesRequired(packageInfo) &&
|
||||
(agentPolicy?.unprivileged_agents ?? 0) > 0
|
||||
)}
|
||||
unprivilegedAgentsCount={agentPolicy?.unprivileged_agents ?? 0}
|
||||
/>
|
||||
)}
|
||||
{formState === 'CONFIRM_UNPRIVILEGED' && agentPolicy ? (
|
||||
<UnprivilegedConfirmModal
|
||||
onCancel={() => setFormState('VALID')}
|
||||
onConfirm={onSubmit}
|
||||
unprivilegedAgentsCount={agentPolicy?.unprivileged_agents ?? 0}
|
||||
agentPolicyName={agentPolicy?.name ?? ''}
|
||||
/>
|
||||
) : null}
|
||||
{formState === 'SUBMITTED_NO_AGENTS' &&
|
||||
agentPolicy &&
|
||||
packageInfo &&
|
||||
|
|
|
@ -19,6 +19,7 @@ export type PackagePolicyFormState =
|
|||
| 'VALID'
|
||||
| 'INVALID'
|
||||
| 'CONFIRM'
|
||||
| 'CONFIRM_UNPRIVILEGED'
|
||||
| 'LOADING'
|
||||
| 'SUBMITTED'
|
||||
| 'SUBMITTED_NO_AGENTS'
|
||||
|
|
|
@ -41,7 +41,10 @@ const POLLING_INTERVAL_MS = 5 * 1000; // 5 sec
|
|||
* @param policyId
|
||||
* @returns agentIds
|
||||
*/
|
||||
export const usePollingAgentCount = (policyId: string, opts?: UsePollingAgentCountOptions) => {
|
||||
export const usePollingAgentCount = (
|
||||
policyId: string,
|
||||
opts?: UsePollingAgentCountOptions
|
||||
): { enrolledAgentIds: string[] } => {
|
||||
const [agentIds, setAgentIds] = useState<string[]>([]);
|
||||
const [didPollInitially, setDidPollInitially] = useState(false);
|
||||
const timeout = useRef<number | undefined>(undefined);
|
||||
|
@ -89,7 +92,7 @@ export const usePollingAgentCount = (policyId: string, opts?: UsePollingAgentCou
|
|||
isAborted = true;
|
||||
};
|
||||
}, [agentIds, policyId, kuery, getNewAgentIds]);
|
||||
return agentIds;
|
||||
return { enrolledAgentIds: agentIds };
|
||||
};
|
||||
|
||||
export const ConfirmAgentEnrollment: React.FunctionComponent<Props> = ({
|
||||
|
|
|
@ -65,19 +65,22 @@ export const AgentEnrollmentConfirmationStep = ({
|
|||
: i18n.translate('xpack.fleet.agentEnrollment.stepAgentEnrollmentConfirmation', {
|
||||
defaultMessage: 'Confirm agent enrollment',
|
||||
}),
|
||||
children:
|
||||
!!isComplete || poll ? (
|
||||
<ConfirmAgentEnrollment
|
||||
policyId={selectedPolicyId}
|
||||
troubleshootLink={troubleshootLink}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
agentCount={agentCount}
|
||||
showLoading={!isComplete || showLoading}
|
||||
isLongEnrollment={isLongEnrollment}
|
||||
/>
|
||||
) : (
|
||||
<AgentEnrollmentPrePollInstructions troubleshootLink={troubleshootLink} />
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{!!isComplete || poll ? (
|
||||
<ConfirmAgentEnrollment
|
||||
policyId={selectedPolicyId}
|
||||
troubleshootLink={troubleshootLink}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
agentCount={agentCount}
|
||||
showLoading={!isComplete || showLoading}
|
||||
isLongEnrollment={isLongEnrollment}
|
||||
/>
|
||||
) : (
|
||||
<AgentEnrollmentPrePollInstructions troubleshootLink={troubleshootLink} />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
status: !isComplete ? 'incomplete' : 'complete',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st
|
|||
|
||||
import type { FullAgentPolicy } from '../../../../common/types/models/agent_policy';
|
||||
import { API_VERSIONS } from '../../../../common/constants';
|
||||
import { getRootIntegrations } from '../../../../common/services';
|
||||
import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../services';
|
||||
|
||||
import { getGcpIntegrationDetailsFromAgentPolicy } from '../../cloud_security_posture/services';
|
||||
|
@ -168,6 +169,7 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
|
|||
installCommand: standaloneInstallCommands,
|
||||
isK8s,
|
||||
cloudSecurityIntegration,
|
||||
rootIntegrations: getRootIntegrations(selectedPolicy?.package_policies ?? []),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -225,7 +227,7 @@ export const ManagedSteps: React.FunctionComponent<InstructionProps> = ({
|
|||
const apiKeyData = apiKey?.data;
|
||||
const enrollToken = apiKey.data ? apiKey.data.item.api_key : '';
|
||||
|
||||
const enrolledAgentIds = usePollingAgentCount(selectedPolicy?.id || '');
|
||||
const { enrolledAgentIds } = usePollingAgentCount(selectedPolicy?.id || '');
|
||||
|
||||
const agentVersion = useAgentVersion();
|
||||
|
||||
|
@ -309,6 +311,7 @@ export const ManagedSteps: React.FunctionComponent<InstructionProps> = ({
|
|||
cloudSecurityIntegration,
|
||||
fleetServerHost,
|
||||
enrollToken,
|
||||
rootIntegrations: getRootIntegrations(selectedPolicy?.package_policies ?? []),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export const InstallManagedAgentStep = ({
|
|||
isComplete,
|
||||
fullCopyButton,
|
||||
onCopy,
|
||||
rootIntegrations,
|
||||
}: {
|
||||
selectedApiKeyId?: string;
|
||||
apiKeyData?: GetOneEnrollmentAPIKeyResponse | null;
|
||||
|
@ -40,6 +41,7 @@ export const InstallManagedAgentStep = ({
|
|||
isComplete?: boolean;
|
||||
fullCopyButton?: boolean;
|
||||
onCopy?: () => void;
|
||||
rootIntegrations?: Array<{ name: string; title: string }>;
|
||||
}): EuiContainedStepProps => {
|
||||
const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled';
|
||||
const status = isComplete ? 'complete' : nonCompleteStatus;
|
||||
|
@ -58,6 +60,7 @@ export const InstallManagedAgentStep = ({
|
|||
onCopy={onCopy}
|
||||
fullCopyButton={fullCopyButton}
|
||||
fleetServerHost={fleetServerHost}
|
||||
rootIntegrations={rootIntegrations}
|
||||
/>
|
||||
) : (
|
||||
<React.Fragment />
|
||||
|
|
|
@ -23,6 +23,7 @@ export const InstallStandaloneAgentStep = ({
|
|||
isComplete,
|
||||
fullCopyButton,
|
||||
onCopy,
|
||||
rootIntegrations,
|
||||
}: {
|
||||
installCommand: CommandsByPlatform;
|
||||
isK8s?: K8sMode;
|
||||
|
@ -30,6 +31,7 @@ export const InstallStandaloneAgentStep = ({
|
|||
isComplete?: boolean;
|
||||
fullCopyButton?: boolean;
|
||||
onCopy?: () => void;
|
||||
rootIntegrations?: Array<{ name: string; title: string }>;
|
||||
}): EuiContainedStepProps => {
|
||||
return {
|
||||
title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', {
|
||||
|
@ -43,6 +45,7 @@ export const InstallStandaloneAgentStep = ({
|
|||
onCopy={onCopy}
|
||||
fullCopyButton={fullCopyButton}
|
||||
isManaged={false}
|
||||
rootIntegrations={rootIntegrations}
|
||||
/>
|
||||
),
|
||||
status: isComplete ? 'complete' : undefined,
|
||||
|
|
|
@ -14,6 +14,8 @@ import { InstallationMessage } from '../agent_enrollment_flyout/installation_mes
|
|||
import type { K8sMode, CloudSecurityIntegration } from '../agent_enrollment_flyout/types';
|
||||
import { PlatformSelector } from '../platform_selector';
|
||||
|
||||
import { RootPrivilegesCallout } from './root_privileges_callout';
|
||||
|
||||
interface Props {
|
||||
installCommand: CommandsByPlatform;
|
||||
isK8s: K8sMode | undefined;
|
||||
|
@ -23,6 +25,7 @@ interface Props {
|
|||
fullCopyButton?: boolean;
|
||||
isManaged?: boolean;
|
||||
onCopy?: () => void;
|
||||
rootIntegrations?: Array<{ name: string; title: string }>;
|
||||
}
|
||||
|
||||
export const InstallSection: React.FunctionComponent<Props> = ({
|
||||
|
@ -34,10 +37,12 @@ export const InstallSection: React.FunctionComponent<Props> = ({
|
|||
fullCopyButton = false,
|
||||
isManaged = true,
|
||||
onCopy,
|
||||
rootIntegrations,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<InstallationMessage isK8s={isK8s} isManaged={isManaged} />
|
||||
<RootPrivilegesCallout rootIntegrations={rootIntegrations} />
|
||||
<PlatformSelector
|
||||
fullCopyButton={fullCopyButton}
|
||||
onCopy={onCopy}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { cleanup, waitFor } from '@testing-library/react';
|
||||
|
||||
import { createFleetTestRendererMock } from '../../mock';
|
||||
|
||||
import { RootPrivilegesCallout } from './root_privileges_callout';
|
||||
|
||||
describe('RootPrivilegesCallout', () => {
|
||||
function render(rootIntegrations?: Array<{ name: string; title: string }>) {
|
||||
cleanup();
|
||||
const renderer = createFleetTestRendererMock();
|
||||
const results = renderer.render(<RootPrivilegesCallout rootIntegrations={rootIntegrations} />);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
it('should render callout requiring root privileges', async () => {
|
||||
const renderResult = render([{ name: 'auditd_manager', title: 'Auditd Manager' }]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.getByText('Root privileges required')).toBeInTheDocument();
|
||||
expect(renderResult.getByTestId('rootPrivilegesCallout').textContent).toContain(
|
||||
'This agent policy contains the following integrations that require Elastic Agents to have root privileges. To ensure that all data required by the integrations can be collected, enroll the agents using an account with root privileges.'
|
||||
);
|
||||
expect(renderResult.getByText('Auditd Manager')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const RootPrivilegesCallout: React.FC<{
|
||||
rootIntegrations?: Array<{ name: string; title: string }>;
|
||||
}> = ({ rootIntegrations = [] }) => {
|
||||
return rootIntegrations.length > 0 ? (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
title={i18n.translate('xpack.fleet.agentEnrollmentCallout.rootPrivilegesTitle', {
|
||||
defaultMessage: 'Root privileges required',
|
||||
})}
|
||||
data-test-subj="rootPrivilegesCallout"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentEnrollmentCallout.rootPrivilegesMessage"
|
||||
defaultMessage="This agent policy contains the following integrations that require Elastic Agents to have root privileges.
|
||||
To ensure that all data required by the integrations can be collected, enroll the agents using an account with root privileges."
|
||||
/>
|
||||
<ul>
|
||||
{rootIntegrations.map((item) => (
|
||||
<li key={item.name}>{item.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null;
|
||||
};
|
|
@ -86,6 +86,7 @@ import {
|
|||
} from './migrations/security_solution/to_v8_11_0_2';
|
||||
import { settingsV1 } from './model_versions/v1';
|
||||
import { packagePolicyV10OnWriteScanFix } from './model_versions/security_solution';
|
||||
import { migratePackagePolicySetRequiresRootToV8150 } from './migrations/to_v8_15_0';
|
||||
|
||||
/*
|
||||
* Saved object types and mappings
|
||||
|
@ -433,6 +434,7 @@ export const getSavedObjectTypes = (
|
|||
name: { type: 'keyword' },
|
||||
title: { type: 'keyword' },
|
||||
version: { type: 'keyword' },
|
||||
requires_root: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
elasticsearch: {
|
||||
|
@ -549,6 +551,20 @@ export const getSavedObjectTypes = (
|
|||
},
|
||||
],
|
||||
},
|
||||
'11': {
|
||||
changes: [
|
||||
{
|
||||
type: 'mappings_addition',
|
||||
addedMappings: {
|
||||
package: { properties: { requires_root: { type: 'boolean' } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'data_backfill',
|
||||
backfillFn: migratePackagePolicySetRequiresRootToV8150,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
'7.10.0': migratePackagePolicyToV7100,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 {
|
||||
createModelVersionTestMigrator,
|
||||
type ModelVersionTestMigrator,
|
||||
} from '@kbn/core-test-helpers-model-versions';
|
||||
|
||||
import type { SavedObject } from '@kbn/core-saved-objects-server';
|
||||
|
||||
import type { PackagePolicy } from '../../../common';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../common';
|
||||
import { getSavedObjectTypes } from '..';
|
||||
|
||||
const getPolicyDoc = (packageName: string): SavedObject<PackagePolicy> => {
|
||||
return {
|
||||
id: 'mock-saved-object-id',
|
||||
attributes: {
|
||||
name: 'Some Policy Name',
|
||||
package: {
|
||||
name: packageName,
|
||||
title: '',
|
||||
version: '',
|
||||
},
|
||||
id: 'package-policy-1',
|
||||
policy_id: '',
|
||||
enabled: true,
|
||||
namespace: '',
|
||||
revision: 0,
|
||||
updated_at: '',
|
||||
updated_by: '',
|
||||
created_at: '',
|
||||
created_by: '',
|
||||
inputs: [],
|
||||
},
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
};
|
||||
};
|
||||
|
||||
describe('8.15.0 Requires Root Package Policy migration', () => {
|
||||
let migrator: ModelVersionTestMigrator;
|
||||
|
||||
beforeEach(() => {
|
||||
migrator = createModelVersionTestMigrator({
|
||||
type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE],
|
||||
});
|
||||
});
|
||||
|
||||
describe('backfilling `requires_root`', () => {
|
||||
it('should backfill `requires_root` field as `true` for `endpoint` package on Kibana update', () => {
|
||||
const migratedPolicyConfigSO = migrator.migrate<PackagePolicy, PackagePolicy>({
|
||||
document: getPolicyDoc('endpoint'),
|
||||
fromVersion: 10,
|
||||
toVersion: 11,
|
||||
});
|
||||
|
||||
expect(migratedPolicyConfigSO.attributes.package?.requires_root).toBe(true);
|
||||
});
|
||||
|
||||
it('should backfill `requires_root` field as `true` for `auditd_manager` package on Kibana update', () => {
|
||||
const migratedPolicyConfigSO = migrator.migrate<PackagePolicy, PackagePolicy>({
|
||||
document: getPolicyDoc('auditd_manager'),
|
||||
fromVersion: 10,
|
||||
toVersion: 11,
|
||||
});
|
||||
|
||||
expect(migratedPolicyConfigSO.attributes.package?.requires_root).toBe(true);
|
||||
});
|
||||
|
||||
it('should not backfill `requires_root` field as `true` for other package on Kibana update', () => {
|
||||
const migratedPolicyConfigSO = migrator.migrate<PackagePolicy, PackagePolicy>({
|
||||
document: getPolicyDoc('other'),
|
||||
fromVersion: 10,
|
||||
toVersion: 11,
|
||||
});
|
||||
|
||||
expect(migratedPolicyConfigSO.attributes.package!.requires_root).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 type {
|
||||
SavedObjectModelDataBackfillFn,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
} from '@kbn/core-saved-objects-server';
|
||||
|
||||
import type { PackagePolicy } from '../../../common';
|
||||
|
||||
// backfill existing package policies with packages requiring root in 8.15.0
|
||||
const ROOT_PACKAGES = [
|
||||
'endpoint',
|
||||
'universal_profiling_agent',
|
||||
'system_audit',
|
||||
'network_traffic',
|
||||
'fim',
|
||||
'auditd_manager',
|
||||
];
|
||||
|
||||
export const migratePackagePolicySetRequiresRootToV8150: SavedObjectModelDataBackfillFn<
|
||||
PackagePolicy,
|
||||
PackagePolicy
|
||||
> = (packagePolicyDoc) => {
|
||||
const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc<PackagePolicy> = packagePolicyDoc;
|
||||
|
||||
if (
|
||||
updatedPackagePolicyDoc.attributes.package &&
|
||||
ROOT_PACKAGES.includes(updatedPackagePolicyDoc.attributes.package.name)
|
||||
) {
|
||||
updatedPackagePolicyDoc.attributes.package.requires_root = true;
|
||||
}
|
||||
|
||||
return { attributes: updatedPackagePolicyDoc.attributes };
|
||||
};
|
|
@ -383,7 +383,10 @@ class AgentPolicyService {
|
|||
throw new FleetError(agentPolicySO.error.message);
|
||||
}
|
||||
|
||||
const agentPolicy = { id: agentPolicySO.id, ...agentPolicySO.attributes };
|
||||
const agentPolicy = {
|
||||
id: agentPolicySO.id,
|
||||
...agentPolicySO.attributes,
|
||||
};
|
||||
|
||||
if (withPackagePolicies) {
|
||||
agentPolicy.package_policies =
|
||||
|
|
|
@ -328,6 +328,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (enrichedPackagePolicy.package && pkgInfo?.agent?.privileges?.root) {
|
||||
enrichedPackagePolicy.package = {
|
||||
...enrichedPackagePolicy.package,
|
||||
requires_root: pkgInfo?.agent?.privileges?.root ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const isoDate = new Date().toISOString();
|
||||
|
@ -457,6 +464,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
: inputs;
|
||||
|
||||
elasticsearch = pkgInfo?.elasticsearch;
|
||||
|
||||
if (packagePolicy.package && pkgInfo?.agent?.privileges?.root) {
|
||||
packagePolicy.package = {
|
||||
...packagePolicy.package,
|
||||
requires_root: pkgInfo?.agent?.privileges?.root ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
policiesToCreate.push({
|
||||
|
@ -862,6 +876,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
assetsMap
|
||||
);
|
||||
elasticsearchPrivileges = pkgInfo.elasticsearch?.privileges;
|
||||
|
||||
if (restOfPackagePolicy.package && pkgInfo?.agent?.privileges?.root) {
|
||||
restOfPackagePolicy.package = {
|
||||
...restOfPackagePolicy.package,
|
||||
requires_root: pkgInfo?.agent?.privileges?.root ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle component template/mappings updates for experimental features, e.g. synthetic source
|
||||
|
@ -1042,6 +1063,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
assetsMap
|
||||
);
|
||||
elasticsearchPrivileges = pkgInfo.elasticsearch?.privileges;
|
||||
|
||||
if (restOfPackagePolicy.package && pkgInfo?.agent?.privileges?.root) {
|
||||
restOfPackagePolicy.package = {
|
||||
...restOfPackagePolicy.package,
|
||||
requires_root: pkgInfo?.agent?.privileges?.root ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -105,6 +105,7 @@ const PackagePolicyBaseSchema = {
|
|||
title: schema.string(),
|
||||
version: schema.string(),
|
||||
experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures),
|
||||
requires_root: schema.maybe(schema.boolean()),
|
||||
})
|
||||
),
|
||||
// Deprecated TODO create remove issue
|
||||
|
@ -148,6 +149,7 @@ const CreatePackagePolicyProps = {
|
|||
title: schema.maybe(schema.string()),
|
||||
version: schema.string(),
|
||||
experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures),
|
||||
requires_root: schema.maybe(schema.boolean()),
|
||||
})
|
||||
),
|
||||
// Deprecated TODO create remove issue
|
||||
|
@ -222,6 +224,7 @@ export const SimplifiedCreatePackagePolicyRequestBodySchema =
|
|||
name: schema.string(),
|
||||
version: schema.string(),
|
||||
experimental_data_stream_features: schema.maybe(ExperimentalDataStreamFeatures),
|
||||
requires_root: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { setupFleetAndAgents } from '../agents/services';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('agent policy with root integrations', () => {
|
||||
skipIfNoDockerRegistry(providerContext);
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
|
||||
});
|
||||
setupFleetAndAgents(providerContext);
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
|
||||
});
|
||||
|
||||
describe('root integrations', () => {
|
||||
before(async () => {
|
||||
await supertest
|
||||
.post(`/api/fleet/epm/packages/auditd_manager/1.16.3`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
});
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/fleet/epm/packages/auditd_manager/1.16.3`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should have root integrations in agent policy response', async () => {
|
||||
// Create agent policy
|
||||
const {
|
||||
body: {
|
||||
item: { id: agentPolicyId },
|
||||
},
|
||||
} = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: `Test policy ${uuidv4()}`,
|
||||
namespace: 'default',
|
||||
monitoring_enabled: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: `auditd-${uuidv4()}`,
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
policy_id: agentPolicyId,
|
||||
package: {
|
||||
name: 'auditd_manager',
|
||||
version: '1.16.3',
|
||||
},
|
||||
inputs: [
|
||||
{
|
||||
type: 'audit/auditd',
|
||||
policy_template: 'auditd',
|
||||
enabled: true,
|
||||
streams: [
|
||||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'logs', dataset: 'auditd_manager.auditd' },
|
||||
vars: {
|
||||
socket_type: { value: '', type: 'select' },
|
||||
immutable: { value: false, type: 'bool' },
|
||||
resolve_ids: { value: true, type: 'bool' },
|
||||
failure_mode: { value: 'silent', type: 'text' },
|
||||
audit_rules: { type: 'textarea' },
|
||||
audit_rule_files: { type: 'text' },
|
||||
preserve_original_event: { value: false, type: 'bool' },
|
||||
backlog_limit: { value: 8192, type: 'text' },
|
||||
rate_limit: { value: 0, type: 'text' },
|
||||
include_warnings: { value: false, type: 'bool' },
|
||||
backpressure_strategy: { value: 'auto', type: 'text' },
|
||||
tags: { value: ['auditd_manager-auditd'], type: 'text' },
|
||||
processors: { type: 'yaml' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Fetch the agent policy
|
||||
const {
|
||||
body: { item: agentPolicy },
|
||||
} = await supertest
|
||||
.get(`/api/fleet/agent_policies/${agentPolicyId}`)
|
||||
.set('kbn-xsrf', 'xxxx');
|
||||
|
||||
// Check that the root integrations are correct
|
||||
expect(
|
||||
Object.values(agentPolicy.package_policies.map((policy: any) => policy.package))
|
||||
).to.eql([
|
||||
{
|
||||
name: 'auditd_manager',
|
||||
title: 'Auditd Manager',
|
||||
requires_root: true,
|
||||
version: '1.16.3',
|
||||
},
|
||||
]);
|
||||
|
||||
// Cleanup agent and package policy
|
||||
await supertest
|
||||
.post(`/api/fleet/agent_policies/delete`)
|
||||
.send({ agentPolicyId })
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -11,5 +11,6 @@ export default function loadTests({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./agent_policy'));
|
||||
loadTestFile(require.resolve('./agent_policy_datastream_permissions'));
|
||||
loadTestFile(require.resolve('./privileges'));
|
||||
loadTestFile(require.resolve('./agent_policy_root_integrations'));
|
||||
});
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -42,6 +42,12 @@ export default function (providerContext: FtrProviderContext) {
|
|||
describe('should respond with correct enrollment settings', async function () {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/fleet/fleet_server');
|
||||
// package verification error without force
|
||||
await supertest
|
||||
.post(`/api/fleet/epm/packages/fleet_server`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/fleet_server');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue