diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.test.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.test.ts index 9d1c0b9bae32e..8bf3984f62faa 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.test.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.test.ts @@ -64,10 +64,14 @@ describe('useSetupTechnology', () => { }); it('calls handleSetupTechnologyChange when setupTechnology changes', () => { + const inputPackage = { + type: 'someType', + policy_template: 'somePolicyTemplate', + } as NewPackagePolicyInput; const handleSetupTechnologyChangeMock = jest.fn(); const { result } = renderHook(() => useSetupTechnology({ - input: { type: 'someType' } as NewPackagePolicyInput, + input: inputPackage, handleSetupTechnologyChange: handleSetupTechnologyChangeMock, }) ); @@ -79,7 +83,10 @@ describe('useSetupTechnology', () => { }); expect(result.current.setupTechnology).toBe(SetupTechnology.AGENTLESS); - expect(handleSetupTechnologyChangeMock).toHaveBeenCalledWith(SetupTechnology.AGENTLESS); + expect(handleSetupTechnologyChangeMock).toHaveBeenCalledWith( + SetupTechnology.AGENTLESS, + inputPackage.policy_template + ); }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts index e18119c3a39de..3f68fb87f2639 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/setup_technology_selector/use_setup_technology.ts @@ -18,7 +18,7 @@ export const useSetupTechnology = ({ }: { input: NewPackagePolicyInput; isAgentlessEnabled?: boolean; - handleSetupTechnologyChange?: (value: SetupTechnology) => void; + handleSetupTechnologyChange?: (value: SetupTechnology, policyTemplateName?: string) => void; isEditPage?: boolean; }) => { const isCspmAws = input.type === CLOUDBEAT_AWS; @@ -34,7 +34,7 @@ export const useSetupTechnology = ({ const updateSetupTechnology = (value: SetupTechnology) => { setSetupTechnology(value); if (handleSetupTechnologyChange) { - handleSetupTechnologyChange(value); + handleSetupTechnologyChange(value, input.policy_template); } }; diff --git a/x-pack/plugins/fleet/common/constants/agentless.ts b/x-pack/plugins/fleet/common/constants/agentless.ts new file mode 100644 index 0000000000000..cbc7e85e563c1 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/agentless.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export const AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION = 'organization'; +export const AGENTLESS_GLOBAL_TAG_NAME_DIVISION = 'division'; +export const AGENTLESS_GLOBAL_TAG_NAME_TEAM = 'team'; diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index 31a7cd6b70686..8ebfe005960c4 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -10,6 +10,7 @@ export { INGEST_SAVED_OBJECT_INDEX, FLEET_SETUP_LOCK_TYPE } from './saved_object export * from './routes'; export * from './agent'; export * from './agent_policy'; +export * from './agentless'; export * from './package_policy'; export * from './epm'; export * from './output'; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index b9e19fbc1947f..3aa65dc3adcd4 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -178,12 +178,18 @@ export interface RegistryImage extends PackageSpecIcon { path: string; } -export interface DeploymentsModesEnablement { +export interface DeploymentsModesDefault { enabled: boolean; } + +export interface DeploymentsModesAgentless extends DeploymentsModesDefault { + organization?: string; + division?: string; + team?: string; +} export interface DeploymentsModes { - agentless: DeploymentsModesEnablement; - default?: DeploymentsModesEnablement; + agentless: DeploymentsModesAgentless; + default?: DeploymentsModesDefault; } export enum RegistryPolicyTemplateKeys { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index 550a288dad371..38663d88e5b23 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -11,6 +11,7 @@ import { waitFor } from '@testing-library/react'; import { createPackagePolicyMock } from '../../../../../../../../common/mocks'; +import type { RegistryPolicyTemplate, PackageInfo } from '../../../../../../../../common/types'; import { SetupTechnology } from '../../../../../../../../common/types'; import { ExperimentalFeaturesService } from '../../../../../services'; import { sendGetOneAgentPolicy, useStartServices, useConfig } from '../../../../../hooks'; @@ -145,6 +146,38 @@ describe('useSetupTechnology', () => { supports_agentless: false, inactivity_timeout: 3600, }; + + const packageInfoMock = { + policy_templates: [ + { + name: 'cspm', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + agentless: { + enabled: true, + organization: 'org', + division: 'div', + team: 'team', + }, + }, + }, + { + name: 'not-cspm', + title: 'Template 2', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + }, + }, + ] as RegistryPolicyTemplate[], + } as PackageInfo; + const packagePolicyMock = createPackagePolicyMock(); const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); @@ -522,4 +555,170 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); }); + + it('should have global_data_tags with the integration team when updating the agentless policy', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + packageInfo: packageInfoMock, + isEditPage: true, + agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any], + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS, 'cspm'); + }); + + waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + ...newAgentPolicyMock, + supports_agentless: true, + global_data_tags: [ + { name: 'organization', value: 'org' }, + { name: 'division', value: 'div' }, + { name: 'team', value: 'team' }, + ], + }); + }); + }); + + it('should not fail and not have global_data_tags when updating the agentless policy when it cannot find the policy template', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + isEditPage: true, + agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any], + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange( + SetupTechnology.AGENTLESS, + 'never-gonna-give-you-up' + ); + }); + + waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + ...newAgentPolicyMock, + supports_agentless: true, + }); + }); + }); + + it('should not fail and not have global_data_tags when updating the agentless policy without the policy temaplte name', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + packageInfo: packageInfoMock, + isEditPage: true, + agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any], + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); + }); + + waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + ...newAgentPolicyMock, + supports_agentless: true, + }); + }); + }); + + it('should not fail and not have global_data_tags when updating the agentless policy without the packageInfo', async () => { + (useConfig as MockFn).mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'https://agentless.api.url', + }, + }, + } as any); + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isCloudEnabled: true, + }, + }); + + const { result } = renderHook(() => + useSetupTechnology({ + setNewAgentPolicy, + newAgentPolicy: newAgentPolicyMock, + updateAgentPolicies: updateAgentPoliciesMock, + setSelectedPolicyTab: setSelectedPolicyTabMock, + packagePolicy: packagePolicyMock, + isEditPage: true, + agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any], + }) + ); + + act(() => { + result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS, 'cspm'); + }); + + waitFor(() => { + expect(setNewAgentPolicy).toHaveBeenCalledWith({ + ...newAgentPolicyMock, + supports_agentless: true, + }); + }); + }); }); 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 241dcfbb93f4e..2a88fecc6b145 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 @@ -19,7 +19,12 @@ import type { import { SetupTechnology } from '../../../../../types'; import { sendGetOneAgentPolicy, useStartServices } from '../../../../../hooks'; import { SelectedPolicyTab } from '../../components'; -import { AGENTLESS_POLICY_ID } from '../../../../../../../../common/constants'; +import { + AGENTLESS_POLICY_ID, + AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, + AGENTLESS_GLOBAL_TAG_NAME_DIVISION, + AGENTLESS_GLOBAL_TAG_NAME_TEAM, +} from '../../../../../../../../common/constants'; import { isAgentlessIntegration as isAgentlessIntegrationFn, getAgentlessAgentPolicyNameFromPackagePolicyName, @@ -150,16 +155,21 @@ export function useSetupTechnology({ }, [isDefaultAgentlessPolicyEnabled]); const handleSetupTechnologyChange = useCallback( - (setupTechnology: SetupTechnology) => { + (setupTechnology: SetupTechnology, policyTemplateName?: string) => { if (!isAgentlessEnabled || setupTechnology === selectedSetupTechnology) { return; } if (setupTechnology === SetupTechnology.AGENTLESS) { if (isAgentlessApiEnabled) { - setNewAgentPolicy(newAgentlessPolicy as NewAgentPolicy); + const agentlessPolicy = { + ...newAgentlessPolicy, + ...getAdditionalAgentlessPolicyInfo(policyTemplateName, packageInfo), + } as NewAgentPolicy; + + setNewAgentPolicy(agentlessPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); - updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); + updateAgentPolicies([agentlessPolicy] as AgentPolicy[]); } // tech debt: remove this when Serverless uses the Agentless API // https://github.com/elastic/security-team/issues/9781 @@ -187,6 +197,7 @@ export function useSetupTechnology({ newAgentlessPolicy, setSelectedPolicyTab, updateAgentPolicies, + packageInfo, ] ); @@ -195,3 +206,37 @@ export function useSetupTechnology({ selectedSetupTechnology, }; } + +const getAdditionalAgentlessPolicyInfo = ( + policyTemplateName?: string, + packageInfo?: PackageInfo +) => { + if (!policyTemplateName || !packageInfo) { + return {}; + } + const agentlessPolicyTemplate = policyTemplateName + ? packageInfo?.policy_templates?.find((policy) => policy.name === policyTemplateName) + : undefined; + + const agentlessInfo = agentlessPolicyTemplate?.deployment_modes?.agentless; + return !agentlessInfo + ? {} + : { + global_data_tags: agentlessInfo + ? [ + { + name: AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, + value: agentlessInfo.organization, + }, + { + name: AGENTLESS_GLOBAL_TAG_NAME_DIVISION, + value: agentlessInfo.division, + }, + { + name: AGENTLESS_GLOBAL_TAG_NAME_TEAM, + value: agentlessInfo.team, + }, + ] + : [], + }; +}; diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 73a62a3cbfb06..fb7e27c8b0ef8 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -16,6 +16,9 @@ export { AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL, AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, AGENT_UPDATE_ACTIONS_INTERVAL_MS, + AGENTLESS_GLOBAL_TAG_NAME_DIVISION, + AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, + AGENTLESS_GLOBAL_TAG_NAME_TEAM, UNPRIVILEGED_AGENT_KUERY, PRIVILEGED_AGENT_KUERY, MAX_TIME_COMPLETE_INSTALL, diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index 42f19d0de85bf..fe8b7a220470d 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -787,6 +787,20 @@ describe('Agentless Agent service', () => { name: 'agentless agent policy', namespace: 'default', supports_agentless: true, + global_data_tags: [ + { + name: 'organization', + value: 'elastic', + }, + { + name: 'division', + value: 'cloud', + }, + { + name: 'team', + value: 'fleet', + }, + ], } as AgentPolicy ); @@ -799,6 +813,11 @@ describe('Agentless Agent service', () => { fleet_url: 'http://fleetserver:8220', policy_id: 'mocked-agentless-agent-policy-id', stack_version: 'mocked-kibana-version-infinite', + labels: { + organization: 'elastic', + division: 'cloud', + team: 'fleet', + }, }), headers: expect.anything(), httpsAgent: expect.anything(), @@ -866,6 +885,20 @@ describe('Agentless Agent service', () => { name: 'agentless agent policy', namespace: 'default', supports_agentless: true, + global_data_tags: [ + { + name: 'organization', + value: 'elastic', + }, + { + name: 'division', + value: 'cloud', + }, + { + name: 'team', + value: 'fleet', + }, + ], } as AgentPolicy ); @@ -877,6 +910,11 @@ describe('Agentless Agent service', () => { fleet_token: 'mocked-fleet-enrollment-api-key', fleet_url: 'http://fleetserver:8220', policy_id: 'mocked-agentless-agent-policy-id', + labels: { + organization: 'elastic', + division: 'cloud', + team: 'fleet', + }, }, headers: expect.anything(), httpsAgent: expect.anything(), @@ -886,6 +924,83 @@ describe('Agentless Agent service', () => { ); }); + it('should create agentless agent when no labels are given', async () => { + const returnValue = { + id: 'mocked', + regional_id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + const soClient = getAgentPolicyCreateMock(); + // ignore unrelated unique name constraint + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest + .spyOn(appContextService, 'getKibanaVersion') + .mockReturnValue('mocked-kibana-version-infinite'); + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + + const createAgentlessAgentReturnValue = await agentlessAgentService.createAgentlessAgent( + esClient, + soClient, + { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy + ); + + expect(axios).toHaveBeenCalledTimes(1); + expect(createAgentlessAgentReturnValue).toEqual(returnValue); + expect(axios).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + fleet_token: 'mocked-fleet-enrollment-api-key', + fleet_url: 'http://fleetserver:8220', + policy_id: 'mocked-agentless-agent-policy-id', + stack_version: 'mocked-kibana-version-infinite', + }), + headers: expect.anything(), + httpsAgent: expect.anything(), + method: 'POST', + url: 'http://api.agentless.com/api/v1/ess/deployments', + }) + ); + }); + it('should delete agentless agent for ESS', async () => { const returnValue = { id: 'mocked', 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 9e6d74ddcf827..7400b5958eb65 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -24,6 +24,11 @@ import { AgentlessAgentCreateError, AgentlessAgentDeleteError, } from '../../errors'; +import { + AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION, + AGENTLESS_GLOBAL_TAG_NAME_DIVISION, + AGENTLESS_GLOBAL_TAG_NAME_TEAM, +} from '../../constants'; import { appContextService } from '../app_context'; @@ -88,12 +93,15 @@ class AgentlessAgentService { ); const tlsConfig = this.createTlsConfig(agentlessConfig); + const labels = this.getAgentlessTags(agentlessAgentPolicy); + const requestConfig: AxiosRequestConfig = { url: prependAgentlessApiBasePathToEndpoint(agentlessConfig, '/deployments'), data: { policy_id: policyId, fleet_url: fleetUrl, fleet_token: fleetToken, + labels, }, method: 'POST', headers: { @@ -203,6 +211,21 @@ class AgentlessAgentService { return response; } + private getAgentlessTags(agentlessAgentPolicy: AgentPolicy) { + if (!agentlessAgentPolicy.global_data_tags) { + return undefined; + } + + const getGlobalTagValueByName = (name: string) => + agentlessAgentPolicy.global_data_tags?.find((tag) => tag.name === name)?.value; + + return { + organization: getGlobalTagValueByName(AGENTLESS_GLOBAL_TAG_NAME_ORGANIZATION), + division: getGlobalTagValueByName(AGENTLESS_GLOBAL_TAG_NAME_DIVISION), + team: getGlobalTagValueByName(AGENTLESS_GLOBAL_TAG_NAME_TEAM), + }; + } + private withRequestIdMessage(message: string, traceId?: string) { return `${message} [Request Id: ${traceId}]`; }