From 6742f770497a946de2d21aa39985243eec2b9f7b Mon Sep 17 00:00:00 2001 From: Lola Date: Tue, 1 Oct 2024 14:38:07 -0400 Subject: [PATCH] [Cloud Security] Agentless integration deletion flow (#191557) ## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. This PR is completes the deletion flow for Agentless CSPM. **Current Agentless Integraton deletion flow**: 1. Successfully delete integration policy 2. Successfully unenrolls agent from agent policy 3. Successfully revokes enrollment token 4. Successfully deletes agentless deployment 5. Successfully deletes agent policy 6. Successful notification shows when deleted integration policy is successful ## Agentless Agent API - Unenrolls agent and revokes token first to avoid 404 save object client error. - Update `is_managed` property to no longer check for `agentPolicy.supports_agentless`. Agentless policies will now be a regular policy. - Adds logging for DELETE agentless Agent API endpoint - Adds agentless API deleteendpoint using try & catch. No errors will be thrown. Agent status will become offline after deployment deletion - If agentless deployment api fails, then we will continue to delete the agent policy ## UI Changes **CSPM Integration** - Updates Agent Policy Error toast notification title - Updates Agent Policy Error toast notification message image **Edit Mode** - Adds back the Agentless selector in Edit Integration image **Integration Policies Page** - Removes automatic navigation to agent policies page when deleting an integration. In 8.17, we have a ticket to [hide the agentless agent policies.](https://github.com/elastic/security-team/issues/9857) - Enables delete button when deleting package policy with agents for agentless policies - Disables Upgrade Action - Removes Add Agent Action image image **Agent Policies Page** - Updates messaging when deleting the agentless policy from agent policy page. Warning users that deleting agentless policy will also delete the integration and unenroll agent. - Enables delete button when deleting agentless policy with agents for agentless policies - Removes Add agent menu action - Removes Upgrade policy menu action - Removes Uninstall agent action - Removes Copy policy menu action image image **Agent Policy Settings** For agent policy that are agentless, we disabled the following [fleet actions:](https://www.elastic.co/guide/en/fleet/current/agent-policy.html#agent-policy-types) - Disables Agent monitoring - Disables Inactivity timeout - Disables Fleet Server - Disables Output for integrations - Disables Output for agent monitoring - Disables Agent binary download - Disables Host name format - Disables Inactive agent unenrollment timeout - Disables Advanced Settings - Limit CPU usage - Disables HTTP monitoring endpoint - Disables Agent Logging image image **Agents Page** - Disables Assign to Policy action - Disables Upgrade Policy action - Removes Unassign agent action - Removes agentless policies where user can add agent to agentless policy image image ### How to test in Serverless Use vault access and open the security Project in [build ]([Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/234438)) ### 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 --- .../components/actions_menu.test.tsx | 66 +++++++ .../agent_policy/components/actions_menu.tsx | 167 ++++++++++------- .../agent_policy_advanced_fields/index.tsx | 26 ++- .../agent_policy_delete_provider.tsx | 50 ++++-- .../components/agent_policy_form.tsx | 2 +- .../steps/components/agent_policy_options.tsx | 58 +++--- .../single_page_layout/hooks/form.tsx | 20 ++- .../hooks/setup_technology.ts | 1 - .../components/header/right_content.tsx | 2 +- .../edit_package_policy_page/index.test.tsx | 23 ++- .../edit_package_policy_page/index.tsx | 17 +- .../components/actions_menu.tsx | 2 +- .../components/table_row_actions.tsx | 11 +- .../components/agent_policy_debugger.tsx | 1 + .../agent_policy_selection.tsx | 12 +- .../package_policy_actions_menu.test.tsx | 43 ++--- .../package_policy_actions_menu.tsx | 22 +-- .../package_policy_delete_provider.tsx | 77 ++++++-- .../public/hooks/use_request/agent_policy.ts | 18 +- .../plugins/fleet/server/errors/handlers.ts | 4 + x-pack/plugins/fleet/server/errors/index.ts | 6 + .../server/services/agent_policy.test.ts | 8 +- .../fleet/server/services/agent_policy.ts | 51 +++++- .../server/services/agent_policy_create.ts | 5 +- .../server/services/agents/agentless_agent.ts | 168 +++++++++++++----- .../fleet/server/services/package_policy.ts | 9 +- .../fleet/server/services/utils/agentless.ts | 19 ++ .../agentless/create_agent.ts | 56 ++++++ .../add_cis_integration_form_page.ts | 7 + .../apis/package_policy/delete.ts | 2 +- ...config.cloud_security_posture.agentless.ts | 16 +- .../agentless/cis_integration_aws.ts | 11 +- .../agentless/cis_integration_gcp.ts | 9 +- 33 files changed, 712 insertions(+), 277 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx index 9781270d9dd81..4df4b4fe912d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.test.tsx @@ -109,6 +109,39 @@ describe('AgentPolicyActionMenu', () => { const deleteButton = result.getByTestId('agentPolicyActionMenuDeleteButton'); expect(deleteButton).toHaveAttribute('disabled'); }); + + it('is disabled when agent policy support agentless is true', () => { + const testRenderer = createFleetTestRendererMock(); + const agentlessPolicy: AgentPolicy = { + ...baseAgentPolicy, + supports_agentless: true, + package_policies: [ + { + id: 'test-package-policy', + is_managed: false, + created_at: new Date().toISOString(), + created_by: 'test', + enabled: true, + inputs: [], + name: 'test-package-policy', + namespace: 'default', + policy_id: 'test', + policy_ids: ['test'], + revision: 1, + updated_at: new Date().toISOString(), + updated_by: 'test', + }, + ], + }; + + const result = testRenderer.render(); + + const agentActionsButton = result.getByTestId('agentActionsBtn'); + agentActionsButton.click(); + + const deleteButton = result.getByTestId('agentPolicyActionMenuDeleteButton'); + expect(deleteButton).not.toHaveAttribute('disabled'); + }); }); describe('add agent', () => { @@ -176,6 +209,39 @@ describe('AgentPolicyActionMenu', () => { const addButton = result.getByTestId('agentPolicyActionMenuAddAgentButton'); expect(addButton).toHaveAttribute('disabled'); }); + + it('should remove add agent button when agent policy support agentless is true', () => { + const testRenderer = createFleetTestRendererMock(); + const agentlessPolicy: AgentPolicy = { + ...baseAgentPolicy, + supports_agentless: true, + package_policies: [ + { + id: 'test-package-policy', + is_managed: false, + created_at: new Date().toISOString(), + created_by: 'test', + enabled: true, + inputs: [], + name: 'test-package-policy', + namespace: 'default', + policy_id: 'test', + policy_ids: ['test'], + revision: 1, + updated_at: new Date().toISOString(), + updated_by: 'test', + }, + ], + }; + + const result = testRenderer.render(); + + const agentActionsButton = result.getByTestId('agentActionsBtn'); + agentActionsButton.click(); + + const addAgentActionButton = result.queryByTestId('agentPolicyActionMenuAddAgentButton'); + expect(addAgentActionButton).toBeNull(); + }); }); describe('add fleet server', () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index 48f391a4e545d..bfb364abf8a5d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -100,82 +100,106 @@ export const AgentPolicyActionMenu = memo<{ ); - const menuItems = agentPolicy?.is_managed - ? [viewPolicyItem] - : [ + const deletePolicyItem = ( + + {(deleteAgentPolicyPrompt) => ( + ) : undefined } - data-test-subj="agentPolicyActionMenuAddAgentButton" - onClick={() => { - setIsContextMenuOpen(false); - setIsEnrollmentFlyoutOpen(true); - }} - key="enrollAgents" - > - {isFleetServerPolicy ? ( - - ) : ( - - )} - , - viewPolicyItem, - { - setIsContextMenuOpen(false); - copyAgentPolicyPrompt(agentPolicy, onCopySuccess); + deleteAgentPolicyPrompt(agentPolicy.id); }} - key="copyPolicy" > - , - - {(deleteAgentPolicyPrompt) => ( - - ) : undefined - } - icon="trash" - onClick={() => { - deleteAgentPolicyPrompt(agentPolicy.id); - }} - > - - - )} - , - ]; + + )} + + ); + + const copyPolicyItem = ( + { + setIsContextMenuOpen(false); + copyAgentPolicyPrompt(agentPolicy, onCopySuccess); + }} + key="copyPolicy" + > + + + ); + + const managedMenuItems = [viewPolicyItem]; + const agentBasedMenuItems = [ + { + setIsContextMenuOpen(false); + setIsEnrollmentFlyoutOpen(true); + }} + key="enrollAgents" + > + {isFleetServerPolicy ? ( + + ) : ( + + )} + , + viewPolicyItem, + copyPolicyItem, + deletePolicyItem, + ]; + const agentlessMenuItems = [viewPolicyItem, deletePolicyItem]; + + let menuItems; + + if (agentPolicy?.is_managed) { + menuItems = managedMenuItems; + } else if (agentPolicy?.supports_agentless) { + menuItems = agentlessMenuItems; + } else { + menuItems = agentBasedMenuItems; + } - if (authz.fleet.allAgents && !agentPolicy?.is_managed) { + if ( + authz.fleet.allAgents && + !agentPolicy?.is_managed && + !agentPolicy?.supports_agentless + ) { menuItems.push( = const licenseService = useLicense(); const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false); const policyHasElasticDefend = useMemo(() => hasElasticDefend(agentPolicy), [agentPolicy]); + const isManagedorAgentlessPolicy = + agentPolicy.is_managed === true || agentPolicy?.supports_agentless === true; const AgentTamperProtectionSectionContent = useMemo( () => ( @@ -196,7 +198,12 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = ); const AgentTamperProtectionSection = useMemo(() => { - if (agentTamperProtectionEnabled && licenseService.isPlatinum() && !agentPolicy.is_managed) { + if ( + agentTamperProtectionEnabled && + licenseService.isPlatinum() && + !agentPolicy.is_managed && + !agentPolicy.supports_agentless + ) { if (AgentTamperProtectionWrapper) { return ( @@ -214,6 +221,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = agentPolicy.is_managed, AgentTamperProtectionWrapper, AgentTamperProtectionSectionContent, + agentPolicy.supports_agentless, ]); return ( @@ -405,7 +413,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = > = > { @@ -582,7 +590,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = isInvalid={Boolean(touchedFields.fleet_server_host_id && validation.fleet_server_host_id)} > = isDisabled={disabled} > = isDisabled={disabled} > = isDisabled={disabled} > = > = > { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx index 58b764ed68add..32c350108bccf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -23,12 +23,13 @@ import { sendGetAgents, } from '../../../hooks'; -import type { PackagePolicy } from '../../../types'; +import type { AgentPolicy, PackagePolicy } from '../../../types'; interface Props { children: (deleteAgentPolicy: DeleteAgentPolicy) => React.ReactElement; hasFleetServer: boolean; packagePolicies?: PackagePolicy[]; + agentPolicy: AgentPolicy; } export type DeleteAgentPolicy = (agentPolicy: string, onSuccess?: OnSuccessCallback) => void; @@ -39,12 +40,13 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ children, hasFleetServer, packagePolicies, + agentPolicy, }) => { const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); - const [agentPolicy, setAgentPolicy] = useState(); + const [agentPolicyId, setAgentPolicyId] = useState(); const [isModalOpen, setIsModalOpen] = useState(false); const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState(false); const [agentsCount, setAgentsCount] = useState(0); @@ -56,20 +58,20 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); const deleteAgentPolicyPrompt: DeleteAgentPolicy = ( - agentPolicyToDelete, + agentPolicyIdToDelete, onSuccess = () => undefined ) => { - if (!agentPolicyToDelete) { + if (!agentPolicyIdToDelete) { throw new Error('No agent policy specified for deletion'); } setIsModalOpen(true); - setAgentPolicy(agentPolicyToDelete); - fetchAgentsCount(agentPolicyToDelete); + setAgentPolicyId(agentPolicyIdToDelete); + fetchAgentsCount(agentPolicyIdToDelete); onSuccessCallback.current = onSuccess; }; const closeModal = () => { - setAgentPolicy(undefined); + setAgentPolicyId(undefined); setIsLoading(false); setIsLoadingAgentsCount(false); setIsModalOpen(false); @@ -80,7 +82,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ try { const { data } = await deleteAgentPolicyMutation.mutateAsync({ - agentPolicyId: agentPolicy!, + agentPolicyId: agentPolicyId!, }); if (data) { @@ -91,13 +93,13 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ }) ); if (onSuccessCallback.current) { - onSuccessCallback.current(agentPolicy!); + onSuccessCallback.current(agentPolicyId!); } } else { notifications.toasts.addDanger( i18n.translate('xpack.fleet.deleteAgentPolicy.failureSingleNotificationTitle', { defaultMessage: "Error deleting agent policy ''{id}''", - values: { id: agentPolicy }, + values: { id: agentPolicyId }, }) ); } @@ -173,7 +175,9 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ ) } buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} + confirmButtonDisabled={ + isLoading || isLoadingAgentsCount || (!agentPolicy?.supports_agentless && !!agentsCount) + } > {packagePoliciesWithMultiplePolicies && ( <> @@ -206,13 +210,23 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ } )} > - + {agentPolicy?.supports_agentless ? ( + {agentPolicy.name}, + }} + /> + ) : ( + + )} ) : hasFleetServer ? ( = ({ ) : null} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx index c39466d779548..29722bbc42850 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/agent_policy_options.tsx @@ -127,34 +127,36 @@ export function useAgentPoliciesOptions(packageInfo?: PackageInfo) { const agentPolicyMultiOptions: Array> = useMemo( () => packageInfo && !isOutputLoading && !isAgentPoliciesLoading && !isLoadingPackagePolicies - ? agentPolicies.map((policy) => { - const isLimitedPackageAlreadyInPolicy = - isPackageLimited(packageInfo!) && - packagePoliciesForThisPackageByAgentPolicyId?.[policy.id]; - - const isAPMPackageAndDataOutputIsLogstash = - packageInfo?.name === FLEET_APM_PACKAGE && - getDataOutputForPolicy(policy)?.type === outputType.Logstash; - - return { - append: isAPMPackageAndDataOutputIsLogstash ? ( - - } - > - - - ) : null, - key: policy.id, - label: policy.name, - disabled: isLimitedPackageAlreadyInPolicy || isAPMPackageAndDataOutputIsLogstash, - 'data-test-subj': 'agentPolicyMultiItem', - }; - }) + ? agentPolicies + .filter((policy) => policy.supports_agentless !== true) + .map((policy) => { + const isLimitedPackageAlreadyInPolicy = + isPackageLimited(packageInfo!) && + packagePoliciesForThisPackageByAgentPolicyId?.[policy.id]; + + const isAPMPackageAndDataOutputIsLogstash = + packageInfo?.name === FLEET_APM_PACKAGE && + getDataOutputForPolicy(policy)?.type === outputType.Logstash; + + return { + append: isAPMPackageAndDataOutputIsLogstash ? ( + + } + > + + + ) : null, + key: policy.id, + label: policy.name, + disabled: isLimitedPackageAlreadyInPolicy || isAPMPackageAndDataOutputIsLogstash, + 'data-test-subj': 'agentPolicyMultiItem', + }; + }) : [], [ packageInfo, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index ca96066facba3..2bae962f48e7c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -315,7 +315,11 @@ export function useOnSubmit({ if ( (agentCount !== 0 || (agentPolicies.length === 0 && selectedPolicyTab !== SelectedPolicyTab.NEW)) && - !(isAgentlessIntegration(packageInfo) || isAgentlessPackagePolicy(packagePolicy)) && + !( + isAgentlessIntegration(packageInfo) || + isAgentlessPackagePolicy(packagePolicy) || + isAgentlessAgentPolicy(overrideCreatedAgentPolicy) + ) && formState !== 'CONFIRM' ) { setFormState('CONFIRM'); @@ -339,10 +343,18 @@ export function useOnSubmit({ } } catch (e) { setFormState('VALID'); + const agentlessPolicy = agentPolicies.find( + (policy) => policy?.supports_agentless === true + ); + notifications.toasts.addError(e, { - title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', { - defaultMessage: 'Unable to create agent policy', - }), + title: agentlessPolicy?.supports_agentless + ? i18n.translate('xpack.fleet.createAgentlessPolicy.errorNotificationTitle', { + defaultMessage: 'Unable to create integration', + }) + : i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', { + defaultMessage: 'Unable to create agent policy', + }), }); return; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 141622410076d..fb6aefcf7583e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -174,7 +174,6 @@ export function useSetupTechnology({ setNewAgentPolicy({ ...newAgentBasedPolicy.current, supports_agentless: false, - is_managed: false, }); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentBasedPolicy.current] as AgentPolicy[]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx index 5dd391450b0cb..6603698a80186 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/header/right_content.tsx @@ -106,7 +106,7 @@ export const HeaderRightContent: React.FunctionComponent { }, }); (sendGetOneAgentPolicy as MockFn).mockResolvedValue({ - data: { item: { id: 'agentless', name: 'Agentless policy', namespace: 'default' } }, + data: { + item: { + id: 'agentless', + name: 'Agentless policy', + namespace: 'default', + supports_agentless: true, + }, + }, }); render(); @@ -514,6 +521,20 @@ describe('edit package policy page', () => { expect(sendUpdatePackagePolicy).toHaveBeenCalled(); }); + it('should hide the multiselect agent policies when agent policy is agentless', async () => { + (useGetAgentPolicies as MockFn).mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', supports_agentless: true }], + }, + isLoading: false, + }); + + await act(async () => { + render(); + }); + expect(renderResult.queryByTestId('agentPolicyMultiSelect')).not.toBeInTheDocument(); + }); + describe('modify agent policies', () => { beforeEach(() => { useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: true }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 2bfdd40a9df2f..e448d1376b2fe 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -46,8 +46,6 @@ import { StepConfigurePackagePolicy, StepDefinePackagePolicy, } from '../create_package_policy_page/components'; - -import { AGENTLESS_POLICY_ID } from '../../../../../../common/constants'; import type { AgentPolicy, PackagePolicyEditExtensionComponentProps } from '../../../types'; import { pkgKeyFromPackageInfo } from '../../../services'; @@ -75,7 +73,6 @@ export const EditPackagePolicyPage = memo(() => { } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); const packagePolicy = useGetOnePackagePolicy(packagePolicyId); - const extensionView = useUIExtension( packagePolicy.data?.item?.package?.name ?? '', 'package-policy-edit' @@ -106,8 +103,7 @@ export const EditPackagePolicyForm = memo<{ } = useConfig(); const { getHref } = useLink(); const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); - const { isAgentlessPackagePolicy } = useAgentless(); - + const { isAgentlessAgentPolicy } = useAgentless(); const { // data agentPolicies: existingAgentPolicies, @@ -130,7 +126,14 @@ export const EditPackagePolicyForm = memo<{ } = usePackagePolicyWithRelatedData(packagePolicyId, { forceUpgrade, }); - const hasAgentlessAgentPolicy = packagePolicy.policy_ids.includes(AGENTLESS_POLICY_ID); + + const hasAgentlessAgentPolicy = useMemo( + () => + existingAgentPolicies.length === 1 + ? existingAgentPolicies.some((policy) => isAgentlessAgentPolicy(policy)) + : false, + [existingAgentPolicies, isAgentlessAgentPolicy] + ); const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; useSetIsReadOnly(!canWriteIntegrationPolicies); @@ -451,7 +454,7 @@ export const EditPackagePolicyForm = memo<{ onChange={handleExtensionViewOnChange} validationResults={validationResults} isEditPage={true} - isAgentlessEnabled={isAgentlessPackagePolicy(packagePolicy)} + isAgentlessEnabled={hasAgentlessAgentPolicy} /> ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx index feefebf4fa2d6..231b10782eca7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx @@ -71,7 +71,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ onClick={() => { setIsReassignFlyoutOpen(true); }} - disabled={!agent.active && !agentPolicy} + disabled={(!agent.active && !agentPolicy) || agentPolicy?.supports_agentless === true} key="reassignPolicy" > { onReassignClick(); }} - disabled={!agent.active} + disabled={!agent.active || agentPolicy?.supports_agentless === true} key="reassignPolicy" > { onUpgradeClick(); }} @@ -138,7 +138,12 @@ export const TableRowActions: React.FunctionComponent<{ ); } - if (authz.fleet.allAgents && agentTamperProtectionEnabled && agent.policy_id) { + if ( + authz.fleet.allAgents && + agentTamperProtectionEnabled && + agent.policy_id && + !agentPolicy?.supports_agentless + ) { menuItems.push( { {selectedPolicyId && ( {(deleteAgentPolicyPrompt) => { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx index e7f951f9c4270..0aea38990f06b 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -33,7 +33,7 @@ const AgentPolicyFormRow = styled(EuiFormRow)` `; type Props = { - agentPolicies: Array>; + agentPolicies: Array>; selectedPolicyId?: string; setSelectedPolicyId: (agentPolicyId?: string) => void; excludeFleetServer?: boolean; @@ -115,10 +115,12 @@ export const AgentPolicySelection: React.FC = (props) => { ({ - value: agentPolicy.id, - text: agentPolicy.name, - }))} + options={agentPolicies + .filter((policy) => !policy?.supports_agentless) + .map((agentPolicy) => ({ + value: agentPolicy.id, + text: agentPolicy.name, + }))} value={selectedPolicyId} onChange={onChangeCallback} aria-label={i18n.translate( diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx index 90e680c2ff845..dbf3969ffc226 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; -import { act, fireEvent } from '@testing-library/react'; +import { act } from '@testing-library/react'; import type { AgentPolicy, InMemoryPackagePolicy } from '../types'; import { createIntegrationsTestRendererMock } from '../mock'; -import { useMultipleAgentPolicies, useStartServices, useLink } from '../hooks'; +import { useMultipleAgentPolicies, useLink } from '../hooks'; import { PackagePolicyActionsMenu } from './package_policy_actions_menu'; @@ -135,6 +135,17 @@ describe('PackagePolicyActionsMenu', () => { }); }); + it('Should not enable upgrade button if package has upgrade and agentless policy is enabled', async () => { + const agentPolicies = createMockAgentPolicies({ supports_agentless: true }); + const packagePolicy = createMockPackagePolicy({ hasUpgrade: true }); + const { utils } = renderMenu({ agentPolicies, packagePolicy }); + + await act(async () => { + const upgradeButton = utils.getByTestId('PackagePolicyActionsUpgradeItem'); + expect(upgradeButton).toBeDisabled(); + }); + }); + it('Should not be able to delete integration from a managed policy', async () => { const agentPolicies = createMockAgentPolicies({ is_managed: true }); const packagePolicy = createMockPackagePolicy(); @@ -154,7 +165,7 @@ describe('PackagePolicyActionsMenu', () => { }); it('Should be able to delete integration from a managed agentless policy', async () => { - const agentPolicies = createMockAgentPolicies({ is_managed: true, supports_agentless: true }); + const agentPolicies = createMockAgentPolicies({ is_managed: false, supports_agentless: true }); const packagePolicy = createMockPackagePolicy(); const { utils } = renderMenu({ agentPolicies, packagePolicy }); await act(async () => { @@ -162,23 +173,6 @@ describe('PackagePolicyActionsMenu', () => { }); }); - it('Should navigate on delete integration when having an agentless policy', async () => { - const agentPolicies = createMockAgentPolicies({ is_managed: true, supports_agentless: true }); - const packagePolicy = createMockPackagePolicy(); - const { utils } = renderMenu({ agentPolicies, packagePolicy }); - - await act(async () => { - fireEvent.click(utils.getByTestId('PackagePolicyActionsDeleteItem')); - }); - await act(async () => { - fireEvent.click(utils.getByTestId('confirmModalConfirmButton')); - }); - expect(useStartServices().application.navigateToApp as jest.Mock).toHaveBeenCalledWith( - 'fleet', - { path: '/policies' } - ); - }); - it('Should show add button if the policy is not managed and showAddAgent=true', async () => { const agentPolicies = createMockAgentPolicies(); const packagePolicy = createMockPackagePolicy({ hasUpgrade: true }); @@ -197,6 +191,15 @@ describe('PackagePolicyActionsMenu', () => { }); }); + it('Should not show add button if the policy is agentless and showAddAgent=true', async () => { + const agentPolicies = createMockAgentPolicies({ supports_agentless: true }); + const packagePolicy = createMockPackagePolicy({ hasUpgrade: true }); + const { utils } = renderMenu({ agentPolicies, packagePolicy, showAddAgent: true }); + await act(async () => { + expect(utils.queryByText('Add agent')).toBeNull(); + }); + }); + it('Should show Edit integration with correct href when agentPolicy is defined', async () => { const agentPolicies = createMockAgentPolicies(); const packagePolicy = createMockPackagePolicy(); diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index fcebfcb2f2475..4da1711b28313 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -10,11 +10,9 @@ import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { AgentPolicy, InMemoryPackagePolicy } from '../types'; -import { useAgentPolicyRefresh, useAuthz, useLink, useStartServices } from '../hooks'; +import { useAgentPolicyRefresh, useAuthz, useLink } from '../hooks'; import { policyHasFleetServer } from '../services'; -import { PLUGIN_ID, pagePathGetters } from '../constants'; - import { AgentEnrollmentFlyout } from './agent_enrollment_flyout'; import { ContextMenuActions } from './context_menu_actions'; import { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; @@ -38,9 +36,6 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const { getHref } = useLink(); const authz = useAuthz(); - const { - application: { navigateToApp }, - } = useStartServices(); const agentPolicy = agentPolicies.length > 0 ? agentPolicies[0] : undefined; // TODO: handle multiple agent policies const canWriteIntegrationPolicies = authz.integrations.writeIntegrationPolicies; @@ -54,7 +49,8 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ const agentPolicyIsManaged = Boolean(agentPolicy?.is_managed); const isOrphanedPolicy = !agentPolicy && packagePolicy.policy_ids.length === 0; - const isAddAgentVisible = showAddAgent && agentPolicy && !agentPolicyIsManaged; + const isAddAgentVisible = + showAddAgent && agentPolicy && !agentPolicyIsManaged && !agentPolicy?.supports_agentless; const onEnrollmentFlyoutClose = useMemo(() => { return () => setIsEnrollmentFlyoutOpen(false); @@ -115,7 +111,10 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ { deletePackagePoliciesPrompt([packagePolicy.id], () => { setIsActionsMenuOpen(false); - if (agentPolicy?.supports_agentless) { - // go back to all agent policies - navigateToApp(PLUGIN_ID, { path: pagePathGetters.policies_list()[1] }); - } else { - refreshAgentPolicy(); - } + refreshAgentPolicy(); }); }} > diff --git a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx index 6369d344a2d9f..7d71915fda252 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx @@ -10,7 +10,12 @@ import { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiIconTip } from '@elastic/eui import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useStartServices, sendDeletePackagePolicy, useConfig } from '../hooks'; +import { + useStartServices, + sendDeletePackagePolicy, + sendDeleteAgentPolicy, + useConfig, +} from '../hooks'; import { AGENTS_PREFIX } from '../../common/constants'; import type { AgentPolicy } from '../types'; import { sendGetAgents, useMultipleAgentPolicies } from '../hooks'; @@ -126,6 +131,26 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ defaultMessage: "Deleted integration ''{id}''", values: { id: successfulResults[0].name || successfulResults[0].id }, }); + + const agentlessPolicy = agentPolicies?.find( + (policy) => policy.supports_agentless === true + ); + + if (!!agentlessPolicy) { + try { + await sendDeleteAgentPolicy({ agentPolicyId: agentlessPolicy.id }); + } catch (e) { + notifications.toasts.addDanger( + i18n.translate( + 'xpack.fleet.deletePackagePolicy.fatalErrorAgentlessNotificationTitle', + { + defaultMessage: 'Error deleting agentless deployment', + } + ) + ); + } + } + notifications.toasts.addSuccess(successMessage); } @@ -155,10 +180,14 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ } closeModal(); }, - [closeModal, packagePolicies, notifications.toasts] + [closeModal, packagePolicies, notifications.toasts, agentPolicies] ); const renderModal = () => { + const isAgentlessPolicy = agentPolicies?.find((policy) => policy?.supports_agentless === true); + const packagePolicy = agentPolicies?.[0]?.package_policies?.find( + (policy) => policy.id === packagePolicies[0] + ); if (!isModalOpen) { return null; } @@ -166,11 +195,18 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ return ( + isAgentlessPolicy ? ( + + ) : ( + + ) } onCancel={closeModal} onConfirm={deletePackagePolicies} @@ -224,14 +260,16 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ + !isAgentlessPolicy && ( + + ) } > - {hasMultipleAgentPolicies ? ( + {hasMultipleAgentPolicies && !isAgentlessPolicy && ( = ({ ), }} /> - ) : ( + )}{' '} + {!hasMultipleAgentPolicies && !isAgentlessPolicy && ( = ({ }} /> )} + {!hasMultipleAgentPolicies && isAgentlessPolicy && ( + {packagePolicy?.name}, + }} + /> + )} ) : null} + {!isLoadingAgentsCount && ( { + return sendRequest({ + path: agentPolicyRouteService.getDeletePath(), + method: 'post', + body: JSON.stringify(body), + version: API_VERSIONS.public.v1, + }); +}; + export function useDeleteAgentPolicyMutation() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: function sendDeleteAgentPolicy(body: DeleteAgentPolicyRequest['body']) { - return sendRequest({ - path: agentPolicyRouteService.getDeletePath(), - method: 'post', - body: JSON.stringify(body), - version: API_VERSIONS.public.v1, - }); - }, + mutationFn: sendDeleteAgentPolicy, onSuccess: () => { return queryClient.invalidateQueries(['agentPolicies']); }, diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index dfd92951c8919..d8971948397d3 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -44,6 +44,7 @@ import { FleetNotFoundError, PackageSavedObjectConflictError, FleetTooManyRequestsError, + AgentlessPolicyExistsRequestError, } from '.'; type IngestErrorHandler = ( @@ -111,6 +112,9 @@ const getHTTPResponseCode = (error: FleetError): number => { if (error instanceof PackageAlreadyInstalledError) { return 409; } + if (error instanceof AgentlessPolicyExistsRequestError) { + return 409; + } // Unsupported Media Type if (error instanceof PackageUnsupportedMediaTypeError) { return 415; diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index 80d8116baaaa3..09b387e7a5cee 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -57,6 +57,12 @@ export class AgentlessAgentCreateError extends FleetError { } } +export class AgentlessPolicyExistsRequestError extends AgentPolicyError { + constructor(message: string) { + super(`Unable to create integration. ${message}`); + } +} + export class AgentPolicyNameExistsError extends AgentPolicyError {} export class AgentReassignmentError extends FleetError {} export class PackagePolicyIneligibleForUpgradeError extends FleetError {} diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 9f42746c9c5fa..00bc01aa1f2cb 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -363,7 +363,7 @@ describe('Agent policy', () => { ); }); - it('should create a policy with is_managed true if agentless feature flag is set and in serverless env', async () => { + it('should create a policy agentless feature flag is set and in serverless env', async () => { jest .spyOn(appContextService, 'getExperimentalFeatures') .mockReturnValue({ agentless: true } as any); @@ -392,7 +392,7 @@ describe('Agent policy', () => { namespace: 'default', supports_agentless: true, status: 'active', - is_managed: true, + is_managed: false, revision: 1, updated_at: expect.anything(), updated_by: 'system', @@ -401,7 +401,7 @@ describe('Agent policy', () => { }); }); - it('should create a policy with is_managed true if agentless feature flag is set and in cloud env', async () => { + it('should create a policy if agentless feature flag is set and in cloud env', async () => { jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); jest.spyOn(appContextService, 'getConfig').mockReturnValue({ agentless: { enabled: true }, @@ -428,7 +428,7 @@ describe('Agent policy', () => { namespace: 'default', supports_agentless: true, status: 'active', - is_managed: true, + is_managed: false, revision: 1, updated_at: expect.anything(), updated_by: 'system', diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 57514ec30052b..ffdb2c4162d52 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -84,6 +84,7 @@ import { FleetUnauthorizedError, HostedAgentPolicyRestrictionRelatedError, PackagePolicyRestrictionRelatedError, + AgentlessPolicyExistsRequestError, } from '../errors'; import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; @@ -113,6 +114,7 @@ import { createSoFindIterable } from './utils/create_so_find_iterable'; import { isAgentlessEnabled } from './utils/agentless'; import { validatePolicyNamespaceForSpace } from './spaces/policy_namespaces'; import { isSpaceAwarenessEnabled } from './spaces/helpers'; +import { agentlessAgentService } from './agents/agentless_agent'; import { scheduleDeployAgentPoliciesTask } from './agent_policies/deploy_agent_policies_task'; const KEY_EDITABLE_FOR_MANAGED_POLICIES = ['namespace']; @@ -387,7 +389,7 @@ class AgentPolicyService { { ...agentPolicy, status: 'active', - is_managed: (agentPolicy.is_managed || agentPolicy?.supports_agentless) ?? false, + is_managed: agentPolicy.is_managed ?? false, revision: 1, updated_at: new Date().toISOString(), updated_by: options?.user?.username || 'system', @@ -411,7 +413,7 @@ class AgentPolicyService { public async requireUniqueName( soClient: SavedObjectsClientContract, - givenPolicy: { id?: string; name: string } + givenPolicy: { id?: string; name: string; supports_agentless?: boolean | null } ) { const savedObjectType = await getAgentPolicySavedObjectType(); @@ -423,7 +425,11 @@ class AgentPolicyService { const idsWithName = results.total && results.saved_objects.map(({ id }) => id); if (Array.isArray(idsWithName)) { const isEditingSelf = givenPolicy.id && idsWithName.includes(givenPolicy.id); - if (!givenPolicy.id || !isEditingSelf) { + + if ( + (!givenPolicy?.supports_agentless && !givenPolicy.id) || + (!givenPolicy?.supports_agentless && !isEditingSelf) + ) { const isSinglePolicy = idsWithName.length === 1; const existClause = isSinglePolicy ? `Agent Policy '${idsWithName[0]}' already exists` @@ -431,6 +437,13 @@ class AgentPolicyService { throw new AgentPolicyNameExistsError(`${existClause} with name '${givenPolicy.name}'`); } + + if (givenPolicy?.supports_agentless && !givenPolicy.id) { + const integrationName = givenPolicy.name.split(' ').pop(); + throw new AgentlessPolicyExistsRequestError( + `${givenPolicy.name} already exist. Please rename the integration name ${integrationName}.` + ); + } } } @@ -661,6 +674,7 @@ class AgentPolicyService { await this.requireUniqueName(soClient, { id, name: agentPolicy.name, + supports_agentless: agentPolicy?.supports_agentless, }); } if (agentPolicy.namespace) { @@ -1141,6 +1155,7 @@ class AgentPolicyService { if (agentPolicy.is_managed && !options?.force) { throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`); } + // Prevent deleting policy when assigned agents are inactive const { total } = await getAgentsByKuery(esClient, soClient, { showInactive: true, @@ -1149,12 +1164,32 @@ class AgentPolicyService { kuery: `${AGENTS_PREFIX}.policy_id:${id}`, }); - if (total > 0) { + if (total > 0 && !agentPolicy?.supports_agentless) { throw new FleetError( 'Cannot delete an agent policy that is assigned to any active or inactive agents' ); } + if (agentPolicy?.supports_agentless) { + logger.debug(`Starting unenrolling agent from agentless policy ${id}`); + // unenroll offline agents for agentless policies first to avoid 404 Save Object error + await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, { + spaceId: soClient.getCurrentNamespace(), + }); + try { + // Deleting agentless deployment + await agentlessAgentService.deleteAgentlessAgent(id); + logger.debug( + `[Agentless API] Successfully deleted agentless deployment for single agent policy id ${id}` + ); + } catch (error) { + logger.error( + `[Agentless API] Error deleting agentless deployment for single agent policy id ${id}` + ); + logger.error(error); + } + } + const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id); if (packagePolicies.length) { @@ -1216,9 +1251,11 @@ class AgentPolicyService { await soClient.delete(savedObjectType, id, { force: true, // need to delete through multiple space }); - await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, { - spaceId: soClient.getCurrentNamespace(), - }); + if (!agentPolicy?.supports_agentless) { + await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, { + spaceId: soClient.getCurrentNamespace(), + }); + } // cleanup .fleet-policies docs on delete await this.deleteFleetServerPoliciesForPolicyId(esClient, id); diff --git a/x-pack/plugins/fleet/server/services/agent_policy_create.ts b/x-pack/plugins/fleet/server/services/agent_policy_create.ts index 4d22820b9aa1c..f370867fc493b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_create.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_create.ts @@ -21,12 +21,11 @@ import { import type { AgentPolicy, NewAgentPolicy } from '../types'; -import { agentlessAgentService } from './agents/agentless_agent'; - import { agentPolicyService, packagePolicyService } from '.'; import { incrementPackageName } from './package_policies'; import { bulkInstallPackages } from './epm/packages'; import { ensureDefaultEnrollmentAPIKeyForAgentPolicy } from './api_keys'; +import { agentlessAgentService } from './agents/agentless_agent'; const FLEET_SERVER_POLICY_ID = 'fleet-server-policy'; @@ -84,7 +83,7 @@ async function createPackagePolicy( user: options.user, bumpRevision: false, authorizationHeader: options.authorizationHeader, - force: options.force || agentPolicy.supports_agentless === true, + force: options.force, }); } diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index c98a5b63e0356..3bf21c3bec0d1 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -24,7 +24,12 @@ import { appContextService } from '../app_context'; import { listEnrollmentApiKeys } from '../api_keys'; import { listFleetServerHosts } from '../fleet_server_host'; -import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless'; +import type { AgentlessConfig } from '../utils/agentless'; +import { + prependAgentlessApiBasePathToEndpoint, + isAgentlessApiEnabled, + getDeletionEndpointPath, +} from '../utils/agentless'; class AgentlessAgentService { public async createAgentlessAgent( @@ -42,23 +47,22 @@ class AgentlessAgentService { }; const logger = appContextService.getLogger(); - logger.debug(`Creating agentless agent ${agentlessAgentPolicy.id}`); + logger.debug(`[Agentless API] Creating agentless agent ${agentlessAgentPolicy.id}`); if (!isAgentlessApiEnabled) { logger.error( - 'Creating agentless agent not supported in non-cloud or non-serverless environments', - errorMetadata + '[Agentless API] Creating agentless agent not supported in non-cloud or non-serverless environments' ); throw new AgentlessAgentCreateError('Agentless agent not supported'); } if (!agentlessAgentPolicy.supports_agentless) { - logger.error('Agentless agent policy does not have agentless enabled'); + logger.error('[Agentless API] Agentless agent policy does not have agentless enabled'); throw new AgentlessAgentCreateError('Agentless agent policy does not have agentless enabled'); } const agentlessConfig = appContextService.getConfig()?.agentless; if (!agentlessConfig) { - logger.error('Missing agentless configuration', errorMetadata); + logger.error('[Agentless API] Missing agentless configuration', errorMetadata); throw new AgentlessAgentCreateError('missing agentless configuration'); } @@ -70,24 +74,16 @@ class AgentlessAgentService { ); logger.debug( - `Creating agentless agent with fleet_url: ${fleetUrl} and fleet_token: [REDACTED]` + `[Agentless API] Creating agentless agent with fleetUrl ${fleetUrl} and fleet_token: [REDACTED]` ); logger.debug( - `Creating agentless agent with TLS cert: ${ + `[Agentless API] Creating agentless agent with TLS cert: ${ agentlessConfig?.api?.tls?.certificate ? '[REDACTED]' : 'undefined' } and TLS key: ${agentlessConfig?.api?.tls?.key ? '[REDACTED]' : 'undefined'} and TLS ca: ${agentlessConfig?.api?.tls?.ca ? '[REDACTED]' : 'undefined'}` ); - - const tlsConfig = new SslConfig( - sslSchema.validate({ - enabled: true, - certificate: agentlessConfig?.api?.tls?.certificate, - key: agentlessConfig?.api?.tls?.key, - certificateAuthorities: agentlessConfig?.api?.tls?.ca, - }) - ); + const tlsConfig = this.createTlsConfig(agentlessConfig); const requestConfig: AxiosRequestConfig = { url: prependAgentlessApiBasePathToEndpoint(agentlessConfig, '/deployments'), @@ -114,33 +110,17 @@ class AgentlessAgentService { requestConfig.data.stack_version = appContextService.getKibanaVersion(); } - const requestConfigDebug = { - ...requestConfig, - data: { - ...requestConfig.data, - fleet_token: '[REDACTED]', - }, - httpsAgent: { - ...requestConfig.httpsAgent, - options: { - ...requestConfig.httpsAgent.options, - cert: requestConfig.httpsAgent.options.cert ? '[REDACTED]' : undefined, - key: requestConfig.httpsAgent.options.key ? '[REDACTED]' : undefined, - ca: requestConfig.httpsAgent.options.ca ? '[REDACTED]' : undefined, - }, - }, - }; - - const requestConfigDebugToString = JSON.stringify(requestConfigDebug); - - logger.debug(`Creating agentless agent with request config ${requestConfigDebugToString}`); + const requestConfigDebugStatus = this.createRequestConfigDebug(requestConfig); + logger.debug( + `[Agentless API] Creating agentless agent with request config ${requestConfigDebugStatus}` + ); const errorMetadataWithRequestConfig: LogMeta = { ...errorMetadata, http: { request: { id: traceId, - body: requestConfigDebug.data, + body: requestConfig.data, }, }, }; @@ -149,7 +129,7 @@ class AgentlessAgentService { (error: Error | AxiosError) => { if (!axios.isAxiosError(error)) { logger.error( - `Creating agentless failed with an error ${error} ${requestConfigDebugToString}`, + `[Agentless API] Creating agentless failed with an error ${error} ${requestConfigDebugStatus}`, errorMetadataWithRequestConfig ); throw new AgentlessAgentCreateError(withRequestIdMessage(error.message)); @@ -160,9 +140,9 @@ class AgentlessAgentService { if (error.response) { // The request was made and the server responded with a status code and error data logger.error( - `Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify( + `[Agentless API] Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify( error.response.status - )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugToString}`, + )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus}`, { ...errorMetadataWithRequestConfig, http: { @@ -180,7 +160,7 @@ class AgentlessAgentService { } else if (error.request) { // The request was made but no response was received logger.error( - `Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugToString}`, + `[Agentless API] Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugStatus}`, errorMetadataWithRequestConfig ); throw new AgentlessAgentCreateError( @@ -189,7 +169,7 @@ class AgentlessAgentService { } else { // Something happened in setting up the request that triggered an Error logger.error( - `Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugToString}`, + `[Agentless API] Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugStatus}`, errorMetadataWithRequestConfig ); throw new AgentlessAgentCreateError( @@ -199,10 +179,110 @@ class AgentlessAgentService { } ); - logger.debug(`Created an agentless agent ${response}`); + logger.debug(`[Agentless API] Created an agentless agent ${response}`); + return response; + } + + public async deleteAgentlessAgent(agentlessPolicyId: string) { + const logger = appContextService.getLogger(); + const agentlessConfig = appContextService.getConfig()?.agentless; + const tlsConfig = this.createTlsConfig(agentlessConfig); + const requestConfig = { + url: getDeletionEndpointPath(agentlessConfig, `/deployments/${agentlessPolicyId}`), + method: 'DELETE', + headers: { + 'Content-type': 'application/json', + }, + httpsAgent: new https.Agent({ + rejectUnauthorized: tlsConfig.rejectUnauthorized, + cert: tlsConfig.certificate, + key: tlsConfig.key, + ca: tlsConfig.certificateAuthorities, + }), + }; + + const requestConfigDebugStatus = this.createRequestConfigDebug(requestConfig); + + logger.debug( + `[Agentless API] Start deleting agentless agent for agent policy ${requestConfigDebugStatus}` + ); + + if (!isAgentlessApiEnabled) { + logger.error( + '[Agentless API] Agentless API is not supported. Deleting agentless agent is not supported in non-cloud or non-serverless environments' + ); + } + + if (!agentlessConfig) { + logger.error('[Agentless API] kibana.yml is currently missing Agentless API configuration'); + } + + logger.debug(`[Agentless API] Deleting agentless agent with TLS config with certificate`); + + logger.debug( + `[Agentless API] Deleting agentless deployment with request config ${requestConfigDebugStatus}` + ); + + const response = await axios(requestConfig).catch((error: AxiosError) => { + const errorLogCodeCause = `${error.code} ${this.convertCauseErrorsToString(error)}`; + + if (!axios.isAxiosError(error)) { + logger.error( + `[Agentless API] Deleting agentless deployment failed with an error ${JSON.stringify( + error + )} ${requestConfigDebugStatus}` + ); + } + if (error.response) { + logger.error( + `[Agentless API] Deleting Agentless deployment Failed Response Error: ${JSON.stringify( + error.response.status + )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugStatus} ` + ); + } else if (error.request) { + logger.error( + `[Agentless API] Deleting agentless deployment failed to receive a response from the Agentless API ${errorLogCodeCause} ${requestConfigDebugStatus}` + ); + } else { + logger.error( + `[Agentless API] Deleting agentless deployment failed to delete the request ${errorLogCodeCause} ${requestConfigDebugStatus}` + ); + } + }); + return response; } + private createTlsConfig(agentlessConfig: AgentlessConfig | undefined) { + return new SslConfig( + sslSchema.validate({ + enabled: true, + certificate: agentlessConfig?.api?.tls?.certificate, + key: agentlessConfig?.api?.tls?.key, + certificateAuthorities: agentlessConfig?.api?.tls?.ca, + }) + ); + } + + private createRequestConfigDebug(requestConfig: AxiosRequestConfig) { + return JSON.stringify({ + ...requestConfig, + data: { + ...requestConfig.data, + fleet_token: '[REDACTED]', + }, + httpsAgent: { + ...requestConfig.httpsAgent, + options: { + ...requestConfig.httpsAgent.options, + cert: requestConfig.httpsAgent.options.cert ? 'REDACTED' : undefined, + key: requestConfig.httpsAgent.options.key ? 'REDACTED' : undefined, + ca: requestConfig.httpsAgent.options.ca ? 'REDACTED' : undefined, + }, + }, + }); + } + private convertCauseErrorsToString = (error: AxiosError) => { if (error.cause instanceof AggregateError) { return error.cause.errors.map((e: Error) => e.message); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 4a7b6c2e2ee70..86d81f3df9b1a 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -1444,12 +1444,6 @@ class PackagePolicyClientImpl implements PackagePolicyClient { }); } - if (agentlessAgentPolicies.length > 0) { - for (const agentPolicyId of agentlessAgentPolicies) { - await agentPolicyService.delete(soClient, esClient, agentPolicyId, { force: true }); - } - } - if (!options?.skipUnassignFromAgentPolicies) { let uniquePolicyIdsR = [ ...new Set( @@ -3021,8 +3015,7 @@ async function validateIsNotHostedPolicy( throw new AgentPolicyNotFoundError('Agent policy not found'); } - const isManagedPolicyWithoutServerlessSupport = - agentPolicy.is_managed && !agentPolicy.supports_agentless && !force; + const isManagedPolicyWithoutServerlessSupport = agentPolicy.is_managed && !force; if (isManagedPolicyWithoutServerlessSupport) { throw new HostedAgentPolicyRestrictionRelatedError( diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index 5c544b1907b25..c85e9cc991a6c 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -28,6 +28,18 @@ const AGENTLESS_SERVERLESS_API_BASE_PATH = '/api/v1/serverless'; type AgentlessApiEndpoints = '/deployments' | `/deployments/${string}`; +export interface AgentlessConfig { + enabled?: boolean; + api?: { + url?: string; + tls?: { + certificate?: string; + key?: string; + ca?: string; + }; + }; +} + export const prependAgentlessApiBasePathToEndpoint = ( agentlessConfig: FleetConfigType['agentless'], endpoint: AgentlessApiEndpoints @@ -38,3 +50,10 @@ export const prependAgentlessApiBasePathToEndpoint = ( : AGENTLESS_ESS_API_BASE_PATH; return `${agentlessConfig.api.url}${endpointPrefix}${endpoint}`; }; + +export const getDeletionEndpointPath = ( + agentlessConfig: FleetConfigType['agentless'], + endpoint: AgentlessApiEndpoints +) => { + return `${agentlessConfig.api.url}${AGENTLESS_ESS_API_BASE_PATH}${endpoint}`; +}; diff --git a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts index 1b1497140875e..2065d1307fbda 100644 --- a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts +++ b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts @@ -73,6 +73,62 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); }); + it(`should show setup technology selector in edit mode`, async () => { + const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); + await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); + + await cisIntegration.inputIntegrationName(integrationPolicyName); + + await cisIntegration.selectSetupTechnology('agentless'); + await cisIntegration.selectAwsCredentials('direct'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.clickSaveButton(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegrationAws.showPostInstallCloudFormationModal()).to.be(false); + + await cisIntegration.navigateToIntegrationCspList(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.navigateToEditIntegrationPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegration.showSetupTechnologyComponent()).to.be(true); + }); + + it(`should hide setup technology selector in edit mode`, async () => { + const integrationPolicyName = `cloud_security_posture1-${new Date().toISOString()}`; + await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); + await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); + + await cisIntegration.inputIntegrationName(integrationPolicyName); + await cisIntegration.selectSetupTechnology('agent-based'); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.clickSaveButton(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegrationAws.showPostInstallCloudFormationModal()).to.be(true); + + await cisIntegration.navigateToIntegrationCspList(); + await pageObjects.header.waitUntilLoadingHasFinished(); + await cisIntegration.navigateToEditIntegrationPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegration.showSetupTechnologyComponent()).to.be(false); + }); + it(`should create default agent-based agent`, async () => { const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts index 8732f0ba5b012..e3ef420055196 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts @@ -285,6 +285,11 @@ export function AddCisIntegrationFormPageProvider({ ); await agentOption.click(); }; + + const showSetupTechnologyComponent = async () => { + return await testSubjects.exists(SETUP_TECHNOLOGY_SELECTOR_ACCORDION_TEST_SUBJ); + }; + const selectAwsCredentials = async (credentialType: 'direct' | 'temporary') => { await clickOptionButton(AWS_CREDENTIAL_SELECTOR); await selectValue( @@ -544,5 +549,7 @@ export function AddCisIntegrationFormPageProvider({ getFirstCspmIntegrationPageAgent, getAgentBasedPolicyValue, showSuccessfulToast, + showSetupTechnologyComponent, + navigateToEditIntegrationPage, }; } diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts index ebe7a91019094..fddf71eaf98a1 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts @@ -166,7 +166,7 @@ export default function (providerContext: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .expect(200); - await supertest.get(`/api/fleet/agent_policies/${agentPolicy.id}`).expect(404); + await supertest.get(`/api/fleet/agent_policies/${agentPolicy.id}`).expect(200); }); }); describe('Delete bulk', () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts index 948a418279ac9..692ae096265fb 100644 --- a/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts +++ b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts @@ -6,8 +6,10 @@ */ import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { createTestConfig } from '../../config.base'; +// TODO: Remove the agentless default config once Serverless API is merged and default policy is deleted export default createTestConfig({ serverlessProject: 'security', junit: { @@ -16,13 +18,23 @@ export default createTestConfig({ kbnServerArgs: [ `--xpack.fleet.packages.0.name=cloud_security_posture`, `--xpack.fleet.packages.0.version=${CLOUD_CREDENTIALS_PACKAGE_VERSION}`, + `--xpack.fleet.agentless.enabled=true`, + `--xpack.fleet.agents.fleet_server.hosts=["https://ftr.kibana:8220"]`, + `--xpack.fleet.internal.fleetServerStandalone=true`, - // Agentless Configuration based on Serverless Security Dev Yaml - config/serverless.security.dev.yml - `--xpack.fleet.enableExperimental.0=agentless`, + // Agentless Configuration based on Serverless Default policy`, `--xpack.fleet.agentPolicies.0.id=agentless`, `--xpack.fleet.agentPolicies.0.name=agentless`, `--xpack.fleet.agentPolicies.0.package_policies=[]`, `--xpack.cloud.serverless.project_id=some_fake_project_id`, + `--xpack.fleet.agentPolicies.0.is_default=true`, + `--xpack.fleet.agentPolicies.0.is_default_fleet_server=true`, + + // Serverless Agentless API + `--xpack.fleet.agentless.api.url=http://localhost:8089`, + `--xpack.fleet.agentless.api.tls.certificate=${KBN_CERT_PATH}`, + `--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`, + `--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`, ], // load tests in the index file testFiles: [require.resolve('./ftr/cloud_security_posture/agentless')], diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts index 6adbbac3cdc57..90991304936ea 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_aws.ts @@ -6,9 +6,11 @@ */ import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; import expect from '@kbn/expect'; - +import * as http from 'http'; import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { setupMockServer } from '../agentless_api/mock_agentless_api'; export default function ({ getPageObjects, getService }: FtrProviderContext) { + const mockAgentlessApiService = setupMockServer(); const pageObjects = getPageObjects([ 'settings', 'common', @@ -24,9 +26,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { let cisIntegration: typeof pageObjects.cisAddIntegration; let cisIntegrationAws: typeof pageObjects.cisAddIntegration.cisAws; let testSubjectIds: typeof pageObjects.cisAddIntegration.testSubjectIds; + let mockApiServer: http.Server; const previousPackageVersion = '1.9.0'; before(async () => { + mockApiServer = mockAgentlessApiService.listen(8089); await pageObjects.svlCommonPage.loginAsAdmin(); cisIntegration = pageObjects.cisAddIntegration; cisIntegrationAws = pageObjects.cisAddIntegration.cisAws; @@ -41,6 +45,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .send({ force: true }) .expect(200); + mockApiServer.close(); }); describe('Serverless - Agentless CIS_AWS Single Account Launch Cloud formation', () => { @@ -110,7 +115,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('Serverless - Agentless CIS_AWS edit flow', () => { + // TODO: Migrate test after Serverless default agentless policy is deleted. + describe.skip('Serverless - Agentless CIS_AWS edit flow', () => { it(`user should save and edit agentless integration policy`, async () => { const newDirectAccessKeyId = `newDirectAccessKey`; @@ -142,7 +148,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ).to.be('true'); }); }); - // FLAKY: https://github.com/elastic/kibana/issues/191017 describe.skip('Serverless - Agentless CIS_AWS Create flow', () => { it(`user should save agentless integration policy when there are no api or validation errors and button is not disabled`, async () => { diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts index cd9e5b2168d1a..85a45f67bf9cc 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless/cis_integration_gcp.ts @@ -7,7 +7,9 @@ import expect from '@kbn/expect'; import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import * as http from 'http'; import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +import { setupMockServer } from '../agentless_api/mock_agentless_api'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'svlCommonPage', 'cisAddIntegration', 'header']); @@ -21,7 +23,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { let cisIntegrationGcp: typeof pageObjects.cisAddIntegration.cisGcp; let testSubjectIds: typeof pageObjects.cisAddIntegration.testSubjectIds; + const mockAgentlessApiService = setupMockServer(); + let mockApiServer: http.Server; + before(async () => { + mockApiServer = mockAgentlessApiService.listen(8089); await pageObjects.svlCommonPage.loginAsAdmin(); cisIntegration = pageObjects.cisAddIntegration; cisIntegrationGcp = pageObjects.cisAddIntegration.cisGcp; @@ -36,6 +42,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .send({ force: true }) .expect(200); + mockApiServer.close(); }); describe('Agentless CIS_GCP Single Account Launch Cloud shell', () => { @@ -93,7 +100,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('Serverless - Agentless CIS_GCP edit flow', () => { + describe.skip('Serverless - Agentless CIS_GCP edit flow', () => { it(`user should save and edit agentless integration policy`, async () => { const newCredentialsJSON = 'newJson'; await cisIntegration.createAgentlessIntegration({