[Fleet] Introduce new config setting xpack.fleet.agentless.isDefault to set agentless deployment by default (#216535)

## Summary

Introduce a new fleet setting `xpack.fleet.agentless.isDefault` for
defaulting the deployment mode to agentless and highlighting the
agentless deployment mode as `Recommended` for the AI4DSOC project.

## Screens recordings

AI4DSOC: 


https://github.com/user-attachments/assets/1fe6df6b-29e0-492c-955e-006e73673322

Otherwise:


https://github.com/user-attachments/assets/e803df49-cbbb-4889-bef1-422abbd6df53

Relates: https://github.com/elastic/security-team/issues/11789
This commit is contained in:
Kylie Meli 2025-04-03 13:11:01 -04:00 committed by GitHub
parent 5402d2b90e
commit 7d3f672f2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 185 additions and 18 deletions

View file

@ -7,3 +7,6 @@ xpack.features.overrides:
### The following features are Security features hidden in Role management UI for this specific tier.
securitySolutionTimeline.hidden: true
securitySolutionNotes.hidden: true
## Agentless deployment by default
xpack.fleet.agentless.isDefault: true

View file

@ -260,6 +260,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean?)',
'xpack.fleet.agents.enabled (boolean?)',
'xpack.fleet.agentless.enabled (boolean?)',
'xpack.fleet.agentless.isDefault (boolean?)',
'xpack.fleet.enableExperimental (array?)',
'xpack.fleet.internal.activeAgentsSoftLimit (number?)',
'xpack.fleet.internal.fleetServerStandalone (boolean?)',

View file

@ -32,6 +32,7 @@ export interface FleetConfigType {
};
agentless?: {
enabled: boolean;
isDefault?: boolean;
api?: {
url?: string;
tls?: {

View file

@ -360,6 +360,7 @@ describe('PackagePolicyInputPanel', () => {
beforeEach(() => {
useAgentlessMock.mockReturnValue({
isAgentlessEnabled: true,
isAgentlessDefault: false,
isAgentlessAgentPolicy: jest.fn(),
isAgentlessIntegration: jest.fn(),
});
@ -392,6 +393,7 @@ describe('PackagePolicyInputPanel', () => {
beforeEach(() => {
useAgentlessMock.mockReturnValue({
isAgentlessEnabled: false,
isAgentlessDefault: false,
isAgentlessAgentPolicy: jest.fn(),
isAgentlessIntegration: jest.fn(),
});

View file

@ -14,6 +14,7 @@ import {
EuiRadioGroup,
EuiDescribedFormGroup,
EuiSpacer,
EuiBadge,
} from '@elastic/eui';
import { SetupTechnology } from '../../../../../types';
@ -25,11 +26,13 @@ export const SetupTechnologySelector = ({
allowedSetupTechnologies,
setupTechnology,
onSetupTechnologyChange,
isAgentlessDefault,
}: {
disabled: boolean;
allowedSetupTechnologies: SetupTechnology[];
setupTechnology: SetupTechnology;
onSetupTechnologyChange: (value: SetupTechnology) => void;
isAgentlessDefault: boolean;
}) => {
return (
<EuiDescribedFormGroup
@ -64,12 +67,21 @@ export const SetupTechnologySelector = ({
id="xpack.fleet.setupTechnology.agentlessInputDisplay"
defaultMessage="Agentless"
/>{' '}
<EuiBetaBadge
label="Beta"
size="s"
tooltipContent="This module is not yet GA. Please help us by reporting any bugs."
alignment="middle"
/>
{isAgentlessDefault ? (
<EuiBadge>
<FormattedMessage
id="xpack.fleet.setupTechnology.agentlessDeployment.recommendedBadge"
defaultMessage="Recommended"
/>
</EuiBadge>
) : (
<EuiBetaBadge
label="Beta"
size="s"
tooltipContent="This module is not yet GA. Please help us by reporting any bugs."
alignment="middle"
/>
)}
</strong>
<EuiText size="s">
<p>

View file

@ -110,6 +110,82 @@ describe('useAgentless', () => {
expect(result.current.isAgentlessEnabled).toBeFalsy();
});
it('should return isAgentlessDefault as falsey when agentless is disabled and isDefault is true', () => {
(useStartServices as MockFn).mockReturnValue({
cloud: {
isServerlessEnabled: true,
isCloudEnabled: false,
},
});
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: false,
isDefault: true,
},
} as any);
const { result } = renderHook(() => useAgentless());
expect(result.current.isAgentlessDefault).toBeFalsy();
});
it('should return isAgentlessDefault as falsey when agentless is enabled and isDefault is false', () => {
(useStartServices as MockFn).mockReturnValue({
cloud: {
isServerlessEnabled: true,
isCloudEnabled: false,
},
});
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
isDefault: false,
},
} as any);
const { result } = renderHook(() => useAgentless());
expect(result.current.isAgentlessDefault).toBeFalsy();
});
it('should return isAgentlessDefault as truthy when agentless is enabled and isDefault is true', () => {
(useStartServices as MockFn).mockReturnValue({
cloud: {
isServerlessEnabled: true,
isCloudEnabled: false,
},
});
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
isDefault: true,
},
} as any);
const { result } = renderHook(() => useAgentless());
expect(result.current.isAgentlessDefault).toBeTruthy();
});
it('should return isAgentlessDefault as falsey when agentless is enabled and isDefault is true, but serverless and cloud disabled', () => {
(useStartServices as MockFn).mockReturnValue({
cloud: {
isServerlessEnabled: false,
isCloudEnabled: false,
},
});
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
isDefault: true,
},
} as any);
const { result } = renderHook(() => useAgentless());
expect(result.current.isAgentlessDefault).toBeFalsy();
});
});
describe('useSetupTechnology', () => {
@ -297,6 +373,62 @@ describe('useSetupTechnology', () => {
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED);
});
it('should be agentless when agentless is enabled and isDefault is true', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
isDefault: 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.allowedSetupTechnologies).toStrictEqual([
SetupTechnology.AGENTLESS,
SetupTechnology.AGENT_BASED,
]);
expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS);
});
it('should be agent-based when agentless is enabled and selected integration is agent-based by default', () => {
(useConfig as MockFn).mockReturnValue({
agentless: {

View file

@ -38,6 +38,7 @@ export const useAgentless = () => {
const isCloud = !!cloud?.isCloudEnabled;
const isAgentlessEnabled = (isCloud || isServerless) && config.agentless?.enabled === true;
const isAgentlessDefault = isAgentlessEnabled && config.agentless?.isDefault === true;
const isAgentlessAgentPolicy = (agentPolicy: AgentPolicy | undefined) => {
if (!agentPolicy) return false;
@ -54,6 +55,7 @@ export const useAgentless = () => {
return {
isAgentlessEnabled,
isAgentlessDefault,
isAgentlessAgentPolicy,
isAgentlessIntegration,
};
@ -80,7 +82,7 @@ export function useSetupTechnology({
agentPolicies?: AgentPolicy[];
integrationToEnable?: string;
}) {
const { isAgentlessEnabled } = useAgentless();
const { isAgentlessEnabled, isAgentlessDefault } = 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 });
@ -102,12 +104,12 @@ export function useSetupTechnology({
const shouldBeDefault =
isAgentlessEnabled &&
(isOnlyAgentlessIntegration(packageInfo, integrationToEnable) ||
isAgentlessSetupDefault(packageInfo, integrationToEnable))
isAgentlessSetupDefault(isAgentlessDefault, packageInfo, integrationToEnable))
? SetupTechnology.AGENTLESS
: SetupTechnology.AGENT_BASED;
setDefaultSetupTechnology(shouldBeDefault);
setSelectedSetupTechnology(shouldBeDefault);
}, [isAgentlessEnabled, packageInfo, integrationToEnable]);
}, [isAgentlessEnabled, isAgentlessDefault, packageInfo, integrationToEnable]);
const agentlessPolicyName = getAgentlessAgentPolicyNameFromPackagePolicyName(packagePolicy.name);
@ -184,15 +186,18 @@ export function useSetupTechnology({
};
}
const isAgentlessSetupDefault = (packageInfo?: PackageInfo, integrationToEnable?: string) => {
const isAgentlessSetupDefault = (
isAgentlessDefault: boolean,
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))
isAgentlessDefault ||
((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;
}

View file

@ -367,7 +367,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
"'package-policy-create' and 'package-policy-replace-define-step' cannot both be registered as UI extensions"
);
}
const { isAgentlessIntegration } = useAgentless();
const { isAgentlessIntegration, isAgentlessDefault } = useAgentless();
const replaceStepConfigurePackagePolicy =
replaceDefineStepView && packageInfo?.name ? (
@ -421,6 +421,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
// agentless doesn't need system integration
setWithSysMonitoring(value === SetupTechnology.AGENT_BASED);
}}
isAgentlessDefault={isAgentlessDefault}
/>
)}
@ -462,6 +463,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
formState,
extensionView,
isAgentlessIntegration,
isAgentlessDefault,
selectedSetupTechnology,
integrationToEnable,
isAgentlessSelected,

View file

@ -110,6 +110,13 @@ describe('Config schema', () => {
});
}).not.toThrow();
});
it('should allow to specify xpack.fleet.agentless.isDefault flag ', () => {
expect(() => {
config.schema.validate({
agentless: { isDefault: true },
});
}).not.toThrow();
});
it('should allow to specify a agentless.api.url in xpack.fleet.agentless.api without the tls config ', () => {
expect(() => {
config.schema.validate({

View file

@ -37,6 +37,7 @@ export const config: PluginConfigDescriptor = {
},
agentless: {
enabled: true,
isDefault: true,
},
enableExperimental: true,
developer: {
@ -151,6 +152,7 @@ export const config: PluginConfigDescriptor = {
agentless: schema.maybe(
schema.object({
enabled: schema.boolean({ defaultValue: false }),
isDefault: schema.maybe(schema.boolean({ defaultValue: false })),
api: schema.maybe(
schema.object({
url: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),