[Cloud Security] Sending the Agentless API the deployment_mode information (#196955)

This commit is contained in:
seanrathier 2024-10-24 09:18:45 -04:00 committed by GitHub
parent b3d5c4b46e
commit 1820eafcdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 420 additions and 11 deletions

View file

@ -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
);
});
});

View file

@ -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);
}
};

View file

@ -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';

View file

@ -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';

View file

@ -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 {

View file

@ -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,
});
});
});
});

View file

@ -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,
},
]
: [],
};
};

View file

@ -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,

View file

@ -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<typeof axios>).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',

View file

@ -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}]`;
}