[UII] Support is_default on integration deployment modes (#208284)

## Summary

Resolves #205761
Resolves https://github.com/elastic/security-team/issues/10712

This PR adds support for `is_default` property for integration
deployment modes, so that if an integration declares agentless to be the
default deployment mode, the policy editor will select `Agentless` by
default when adding it:

<img width="1422" alt="image"
src="https://github.com/user-attachments/assets/74c7c8fa-2df9-4e03-bb2b-aa192927aa6d"
/>

## Testing

1. Check out `integrations` repo
2. Apply
[cspm-patch.diff](https://gist.github.com/jen-huang/8574262ea2e5fb8b1c3a134d2b4263d5)
3. In `packages/cloud_security_posture/`, run `elastic-package build`
(or ask Jen for the built .zip if you have trouble 😉)
4. Upload the built package in Kibana at
`/app/integrations/create/upload`
5. Go to Integrations > search for CSPM > Add integration
6. Observe that Advanced options > Setup technology dropdown defaults to
Agentless and agent policy selection is not offered
7. Go back to Integrations > search for Security Posture Management
(parent of CSPM) > Add integration
8. Observe that CSPM is selected by default with Agentless mode selected
by default as well

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [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
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Jen Huang 2025-01-28 00:26:03 -08:00 committed by GitHub
parent c347582ffb
commit 2d3fc93e74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 427 additions and 108 deletions

View file

@ -151,6 +151,40 @@ describe('agentless_policy_helper', () => {
expect(result).toBe(true);
});
it('should return true if packageInfo is defined and selected integration only has agentless', () => {
const packageInfo = {
policy_templates: [
{
name: 'template1',
title: 'Template 1',
description: '',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
{
name: 'template2',
title: 'Template 2',
description: '',
deployment_modes: {
agentless: {
enabled: true,
},
},
},
] as RegistryPolicyTemplate[],
};
const result = isOnlyAgentlessIntegration(packageInfo, 'template2');
expect(result).toBe(true);
});
it('should return false if packageInfo is defined but has other deployment types', () => {
const packageInfo = {
policy_templates: [

View file

@ -31,14 +31,18 @@ export const getAgentlessAgentPolicyNameFromPackagePolicyName = (packagePolicyNa
};
export const isOnlyAgentlessIntegration = (
packageInfo: Pick<PackageInfo, 'policy_templates'> | undefined
packageInfo?: Pick<PackageInfo, 'policy_templates'>,
integrationToEnable?: string
) => {
if (
packageInfo?.policy_templates &&
packageInfo?.policy_templates.length > 0 &&
packageInfo?.policy_templates.every((policyTemplate) =>
isOnlyAgentlessPolicyTemplate(policyTemplate)
)
packageInfo &&
packageInfo.policy_templates &&
packageInfo.policy_templates?.length > 0 &&
((integrationToEnable &&
packageInfo.policy_templates?.find(
(p) => p.name === integrationToEnable && isOnlyAgentlessPolicyTemplate(p)
)) ||
packageInfo.policy_templates?.every((p) => isOnlyAgentlessPolicyTemplate(p)))
) {
return true;
}

View file

@ -195,6 +195,7 @@ export interface RegistryImage extends PackageSpecIcon {
export interface DeploymentsModesDefault {
enabled: boolean;
is_default?: boolean;
}
export interface DeploymentsModesAgentless extends DeploymentsModesDefault {

View file

@ -312,6 +312,7 @@ export function useOnSubmit({
setSelectedPolicyTab,
packageInfo,
packagePolicy,
integrationToEnable,
});
const setupTechnologyRef = useRef<SetupTechnology | undefined>(selectedSetupTechnology);
// sync the inputs with the agentless selector change

View file

@ -227,18 +227,305 @@ describe('useSetupTechnology', () => {
jest.clearAllMocks();
});
it('should initialize with default values when agentless is disabled', () => {
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
describe('default values', () => {
it('should be agent-based when agentless is disabled', () => {
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
});
it('should be agent-based when agentless is enabled and integrations have a mix of deployment modes', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
packageInfo: {
policy_templates: [
{
name: 'template1',
title: 'Template 1',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
{
name: 'template2',
title: 'Template 2',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
],
} as PackageInfo,
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
});
it('should be agent-based when agentless is enabled and selected integration is agent-based by default', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
packageInfo: {
policy_templates: [
{
name: 'template1',
title: 'Template 1',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
{
name: 'template2',
title: 'Template 2',
deployment_modes: {
default: {
enabled: true,
is_default: true,
},
agentless: {
enabled: true,
},
},
},
],
} as PackageInfo,
integrationToEnable: 'template2',
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
});
it('should be agent-based when packageInfo has no policy templates', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
packageInfo: {
policy_templates: [] as PackageInfo['policy_templates'],
} as PackageInfo,
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
});
it('should be agentless when agentless is enabled and all integrations are only agentless', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
packageInfo: {
policy_templates: [
{
name: 'template1',
title: 'Template 1',
deployment_modes: {
default: {
enabled: false,
},
agentless: {
enabled: true,
},
},
},
{
name: 'template2',
title: 'Template 2',
deployment_modes: {
default: {
enabled: false,
},
agentless: {
enabled: true,
},
},
},
],
} as PackageInfo,
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
});
it('should be agentless when agentless is enabled and selected integration is only agentless', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
packageInfo: {
policy_templates: [
{
name: 'template1',
title: 'Template 1',
deployment_modes: {
default: {
enabled: false,
},
agentless: {
enabled: true,
},
},
},
{
name: 'template2',
title: 'Template 2',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
],
} as PackageInfo,
integrationToEnable: 'template1',
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
});
it('should be agentless when agentless is enabled and selected integration is agentless by default', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
updatePackagePolicy: updatePackagePolicyMock,
packageInfo: {
policy_templates: [
{
name: 'template1',
title: 'Template 1',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
is_default: true,
},
},
},
{
name: 'template2',
title: 'Template 2',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
],
} as PackageInfo,
integrationToEnable: 'template1',
})
);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
});
});
it('should set agentless setup technology if agent policy supports agentless in edit page', async () => {
@ -270,7 +557,7 @@ describe('useSetupTechnology', () => {
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
});
it('should create agentless policy if isCloud and agentless.enabled', async () => {
it('should create agentless policy if isCloud and agentless.enabled', async () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useRef, useState, useEffect } from 'react';
import { useConfig } from '../../../../../hooks';
import { generateNewAgentPolicyWithDefaults } from '../../../../../../../../common/services/generate_new_agent_policy';
@ -68,6 +68,7 @@ export function useSetupTechnology({
packagePolicy,
isEditPage,
agentPolicies,
integrationToEnable,
}: {
setNewAgentPolicy: (policy: NewAgentPolicy) => void;
newAgentPolicy: NewAgentPolicy;
@ -77,19 +78,30 @@ export function useSetupTechnology({
packagePolicy: NewPackagePolicy;
isEditPage?: boolean;
agentPolicies?: AgentPolicy[];
integrationToEnable?: string;
}) {
const { isAgentlessEnabled } = useAgentless();
// this is a placeholder for the new agent-BASED policy that will be used when the user switches from agentless to agent-based and back
const orginalAgentPolicyRef = useRef<NewAgentPolicy>({ ...newAgentPolicy });
const [currentAgentPolicy, setCurrentAgentPolicy] = useState(newAgentPolicy);
const defaultSetupTechnology = useMemo(() => {
return isOnlyAgentlessIntegration(packageInfo) || isAgentlessSetupDefault(packageInfo)
? SetupTechnology.AGENTLESS
: SetupTechnology.AGENT_BASED;
}, [packageInfo]);
const [selectedSetupTechnology, setSelectedSetupTechnology] =
useState<SetupTechnology>(defaultSetupTechnology);
const [selectedSetupTechnology, setSelectedSetupTechnology] = useState<SetupTechnology>(
SetupTechnology.AGENT_BASED
);
// derive default setup technology based on package info and selected integration
const [defaultSetupTechnology, setDefaultSetupTechnology] = useState<SetupTechnology>(
SetupTechnology.AGENT_BASED
);
useEffect(() => {
const shouldBeDefault =
isOnlyAgentlessIntegration(packageInfo, integrationToEnable) ||
isAgentlessSetupDefault(packageInfo, integrationToEnable)
? SetupTechnology.AGENTLESS
: SetupTechnology.AGENT_BASED;
setDefaultSetupTechnology(shouldBeDefault);
setSelectedSetupTechnology(shouldBeDefault);
}, [packageInfo, integrationToEnable]);
const agentlessPolicyName = getAgentlessAgentPolicyNameFromPackagePolicyName(packagePolicy.name);
@ -165,9 +177,19 @@ export function useSetupTechnology({
};
}
const isAgentlessSetupDefault = (packageInfo?: PackageInfo) => {
// TODO: https://github.com/elastic/kibana/issues/205761
// placeholder for the logic to determine if the agentless setup is the default
const isAgentlessSetupDefault = (packageInfo?: PackageInfo, integrationToEnable?: string) => {
if (
packageInfo &&
packageInfo.policy_templates &&
packageInfo.policy_templates.length > 0 &&
((integrationToEnable &&
packageInfo?.policy_templates?.find((p) => p.name === integrationToEnable)?.deployment_modes
?.agentless.is_default) ||
packageInfo?.policy_templates?.every((p) => p.deployment_modes?.agentless.is_default))
) {
return true;
}
return false;
};

View file

@ -145,15 +145,16 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
const [agentCount, setAgentCount] = useState<number>(0);
const integrationInfo = useMemo(
() =>
(params as AddToPolicyParams).integration
? packageInfo?.policy_templates?.find(
(policyTemplate) => policyTemplate.name === (params as AddToPolicyParams).integration
)
: undefined,
[packageInfo?.policy_templates, params]
const [integrationToEnable, setIntegrationToEnable] = useState<string | undefined>(
params.integration
);
const integrationInfo = useMemo(() => {
return integrationToEnable
? packageInfo?.policy_templates?.find(
(policyTemplate) => policyTemplate.name === integrationToEnable
)
: undefined;
}, [integrationToEnable, packageInfo?.policy_templates]);
const showSecretsDisabledCallout =
!fleetStatus.isSecretsStorageEnabled &&
@ -187,7 +188,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
selectedPolicyTab,
withSysMonitoring,
queryParamsPolicyId,
integrationToEnable: integrationInfo?.name,
integrationToEnable,
hasFleetAddAgentsPrivileges,
setNewAgentPolicy,
setSelectedPolicyTab,
@ -374,6 +375,8 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
handleSetupTechnologyChange={handleSetupTechnologyChange}
isAgentlessEnabled={isAgentlessIntegration(packageInfo)}
defaultSetupTechnology={defaultSetupTechnology}
integrationToEnable={integrationToEnable}
setIntegrationToEnable={setIntegrationToEnable}
/>
</ExtensionWrapper>
)
@ -414,7 +417,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
{!extensionView && (
<StepConfigurePackagePolicy
packageInfo={packageInfo}
showOnlyIntegration={integrationInfo?.name}
showOnlyIntegration={integrationToEnable}
packagePolicy={packagePolicy}
updatePackagePolicy={updatePackagePolicy}
validationResults={validationResults}
@ -449,7 +452,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
extensionView,
isAgentlessIntegration,
selectedSetupTechnology,
integrationInfo?.name,
integrationToEnable,
isAgentlessSelected,
handleExtensionViewOnChange,
handleSetupTechnologyChange,
@ -465,18 +468,19 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
children: replaceStepConfigurePackagePolicy || stepConfigurePackagePolicy,
headingElement: 'h2',
},
...(selectedSetupTechnology !== SetupTechnology.AGENTLESS
? [
{
title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
defaultMessage: 'Where to add this integration?',
}),
children: stepSelectAgentPolicy,
headingElement: 'h2',
},
]
: []),
];
if (selectedSetupTechnology !== SetupTechnology.AGENTLESS) {
steps.push({
title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
defaultMessage: 'Where to add this integration?',
}),
children: stepSelectAgentPolicy,
headingElement: 'h2',
});
}
// Display package error if there is one
if (packageInfoError) {
return (

View file

@ -46,6 +46,8 @@ export type PackagePolicyReplaceDefineStepExtensionComponentProps = (
isAgentlessEnabled?: boolean;
handleSetupTechnologyChange?: (setupTechnology: SetupTechnology) => void;
defaultSetupTechnology?: SetupTechnology;
integrationToEnable?: string;
setIntegrationToEnable?: (integration: string) => void;
};
/**

View file

@ -414,18 +414,6 @@ describe('<CspPolicyTemplateForm />', () => {
});
// 2nd call happens on mount and increments kspm template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyK8s(),
inputs: policy.inputs.map((input) => ({
...input,
enabled: input.policy_template === 'kspm',
})),
name: 'kspm-1',
},
});
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
@ -502,19 +490,6 @@ describe('<CspPolicyTemplateForm />', () => {
});
// 2nd call happens on mount and increments vuln_mgmt template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyVulnMgmtAWS(),
inputs: policy.inputs.map((input) => ({
...input,
enabled: input.policy_template === 'vuln_mgmt',
})),
name: 'vuln_mgmt-1',
},
});
// 3rd call happens on mount and increments vuln_mgmt template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
@ -589,19 +564,6 @@ describe('<CspPolicyTemplateForm />', () => {
});
// 2nd call happens on mount and increments cspm template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {
...getMockPolicyAWS(),
inputs: policy.inputs.map((input) => ({
...input,
enabled: input.policy_template === 'cspm',
})),
name: 'cspm-1',
},
});
// // 3rd call happens on mount and increments cspm template enabled input
expect(onChange).toHaveBeenCalledWith({
isValid: true,
updatedPolicy: {

View file

@ -671,12 +671,16 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
handleSetupTechnologyChange,
isAgentlessEnabled,
defaultSetupTechnology,
integrationToEnable,
setIntegrationToEnable,
}) => {
const integrationParam = useParams<{ integration: CloudSecurityPolicyTemplate }>().integration;
const integration = SUPPORTED_POLICY_TEMPLATES.includes(integrationParam)
? integrationParam
: undefined;
const isParentSecurityPosture = !integration;
const integration =
integrationToEnable &&
SUPPORTED_POLICY_TEMPLATES.includes(integrationToEnable as CloudSecurityPolicyTemplate)
? integrationToEnable
: undefined;
const isParentSecurityPosture = !integrationParam;
// Handling validation state
const [isValid, setIsValid] = useState(true);
const { cloud } = useKibana().services;
@ -803,18 +807,6 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
// Required for mount only to ensure a single input type is selected
// This will remove errors in validationResults.vars
setEnabledPolicyInput(DEFAULT_INPUT_TYPE[input.policy_template]);
// When the integration is the parent Security Posture (!integration) we need to
// reset the setup technology when the integration option changes if it was set to agentless for CSPM
if (isParentSecurityPosture && input.policy_template !== 'cspm') {
updateSetupTechnology(SetupTechnology.AGENT_BASED);
} else if (
isParentSecurityPosture &&
input.policy_template === 'cspm' &&
defaultSetupTechnology
) {
updateSetupTechnology(defaultSetupTechnology);
}
refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, input.policy_template, isEditPage]);
@ -825,6 +817,7 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
}
setEnabledPolicyInput(input.type);
setIntegrationToEnable?.(input.policy_template);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setupTechnology]);
@ -840,7 +833,7 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
packagePolicyList: packagePolicyList?.items,
isEditPage,
isLoading,
integration,
integration: integration as CloudSecurityPolicyTemplate,
newPolicy,
updatePolicy,
setCanFetchIntegration,
@ -889,12 +882,15 @@ export const CspPolicyTemplateForm = memo<PackagePolicyReplaceDefineStepExtensio
<>
{isEditPage && <EditScreenStepTitle />}
{/* Defines the enabled policy template */}
{!integration && (
{isParentSecurityPosture && (
<>
<PolicyTemplateSelector
selectedTemplate={input.policy_template}
policy={newPolicy}
setPolicyTemplate={(template) => setEnabledPolicyInput(DEFAULT_INPUT_TYPE[template])}
setPolicyTemplate={(template) => {
setEnabledPolicyInput(DEFAULT_INPUT_TYPE[template]);
setIntegrationToEnable?.(template);
}}
disabled={isEditPage}
/>
<EuiSpacer size="l" />

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { NewPackagePolicyInput } from '@kbn/fleet-plugin/common';
import { SetupTechnology } from '@kbn/fleet-plugin/public';
@ -35,6 +35,12 @@ export const useSetupTechnology = ({
defaultSetupTechnology || defaultEditSetupTechnology
);
// Default setup technology may update asynchrounously as data loads from
// parent component, or when integration is changed, so re-set state if it changes
useEffect(() => {
setSetupTechnology(defaultSetupTechnology || defaultEditSetupTechnology);
}, [defaultEditSetupTechnology, defaultSetupTechnology]);
const updateSetupTechnology = (value: SetupTechnology) => {
setSetupTechnology(value);
if (handleSetupTechnologyChange) {