Skip to content

Commit

Permalink
[Defend Workflows][Reusable integrations] Handling resuable integrati…
Browse files Browse the repository at this point in the history
…ons on endpoint onboarding page (elastic#193518)

## Summary

During onboarding on the Endpoint list page, if the user already created
at least one Elastic Defend integration, we show this screen (original):
<img width="400" alt="image"
src="https://github.com/user-attachments/assets/98f04002-7c18-48bf-8b29-a4ad6113385c">

Due to the new enterprise level feature added by Fleet team, the
reusable integrations, now an integration policy can be assigned **zero,
one or more agent policies**, and this PR's goal is to update this
onboarding screen to tackle these changes.

### One integration is added to more than one Agent Policies
When creating/editing an integration, it can be added to more than one
Agent Policies:

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/f0982e28-11b0-4aef-a059-6c952e0e33b7">

#### ✅ Solution for usecase
To be able to select where to enroll an Agent, now they are listed as
`Package policy - Agent policy` pairs.
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/c82d70ed-5b87-43b5-ab16-3c4549373b32">

### Some integrations are not added to an Agent Policy
<img width="400" alt="image"
src="https://github.com/user-attachments/assets/108ccd78-d019-42ca-a92a-905344172d09">

#### ✅ Solution for usecase
A new callout is added to indicate to the user that there are some
integrations that cannot be deployed to an Agent.

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/3bedc56b-70a3-4f4f-9881-e91ae458cadf">

Clicking on the Integrations opens their editing page in a new tab.
Clickin on the 'Elastic Defend Integration policies' opens the Defend
integration's policies tab in a new browser tab.

### None of the integrations are added to an Agent Policy
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/39fd2efc-2a96-4109-9311-b666bd44ca1f">

#### ✅ Solution for usecase
Another 'warning' callout is displayed indicating that there are no
usable integrations. This, combined with the other callout hopefully
help the user to go forward.

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/78efc1ea-dd3e-4aff-bd67-b1e37a2508e1">


### RBAC
In case the user doesn't hold the required privileges, the same screen
is displayed as when there are no hosts and no policies, or there are
policies but no hosts. Just as before.
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/814f460d-7e17-49a7-8337-9cf1f8a8f0ef">


### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [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] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))

---------

Co-authored-by: Elastic Machine <[email protected]>
Co-authored-by: Joe Peeples <[email protected]>
  • Loading branch information
3 people authored Oct 1, 2024
1 parent d570d60 commit 8eceb0d
Show file tree
Hide file tree
Showing 11 changed files with 672 additions and 300 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type PartialPackagePolicy = Partial<Omit<PackagePolicy, 'inputs'>> & {
inputs?: PackagePolicy['inputs'];
};

type PartialEndpointPolicyData = Partial<Omit<PolicyData, 'inputs'>> & {
export type PartialEndpointPolicyData = Partial<Omit<PolicyData, 'inputs'>> & {
inputs?: PolicyData['inputs'];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import { firstNonNullValue } from './models/ecs_safety_helpers';
import type { EventOptions } from './types/generator';
import { BaseDataGenerator } from './data_generators/base_data_generator';
import type { PartialEndpointPolicyData } from './data_generators/fleet_package_policy_generator';
import { FleetPackagePolicyGenerator } from './data_generators/fleet_package_policy_generator';

export type Event = AlertEvent | SafeEndpointEvent;
Expand Down Expand Up @@ -1581,8 +1582,14 @@ export class EndpointDocGenerator extends BaseDataGenerator {
/**
* Generates a Fleet `package policy` that includes the Endpoint Policy data
*/
public generatePolicyPackagePolicy(seed: string = 'seed'): PolicyData {
return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy();
public generatePolicyPackagePolicy({
seed,
overrides,
}: {
seed?: string;
overrides?: PartialEndpointPolicyData;
} = {}): PolicyData {
return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy(overrides);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ import {
EuiLoadingSpinner,
EuiLink,
EuiSkeletonText,
EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import type { ImmutableArray, PolicyData } from '../../../common/endpoint/types';
import { useUserPrivileges } from '../../common/components/user_privileges';
import onboardingLogo from '../images/security_administration_onboarding.svg';
import { useKibana } from '../../common/lib/kibana';
import { useAppUrl, useKibana } from '../../common/lib/kibana';

const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({
textAlign: 'center',
Expand Down Expand Up @@ -103,12 +107,12 @@ const PolicyEmptyState = React.memo<{
{policyEntryPoint ? (
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromPolicyPage"
defaultMessage="From this page, you’ll be able to view and manage the Elastic Defend Integration policies in your environment running Elastic Defend."
defaultMessage="From this page, you can view and manage the Elastic Defend integration policies in your environment running Elastic Defend."
/>
) : (
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromEndpointPage"
defaultMessage="From this page, you’ll be able to view and manage the hosts in your environment running Elastic Defend."
defaultMessage="From this page, you can view and manage the hosts in your environment running Elastic Defend."
/>
)}
</EuiText>
Expand Down Expand Up @@ -170,107 +174,216 @@ const EndpointsEmptyState = React.memo<{
actionDisabled: boolean;
handleSelectableOnChange: (o: EuiSelectableProps['options']) => void;
selectionOptions: EuiSelectableProps['options'];
}>(({ loading, onActionClick, actionDisabled, handleSelectableOnChange, selectionOptions }) => {
const policySteps = useMemo(
() => [
{
title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', {
defaultMessage: 'Select the integration you want to use',
}),
children: (
policyItems: ImmutableArray<PolicyData>;
}>(
({
loading,
onActionClick,
actionDisabled,
handleSelectableOnChange,
selectionOptions,
policyItems,
}) => {
const { getAppUrl } = useAppUrl();
const policyItemsWithoutAgentPolicy = useMemo(
() => policyItems.filter((policy) => !policy.policy_ids.length),
[policyItems]
);

const policiesNotAddedToAgentPolicyCallout = useMemo(
() =>
!!policyItemsWithoutAgentPolicy.length && (
<>
<EuiText color="subdued" size="m" grow={false}>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.stepOne"
defaultMessage="Select from existing integrations. This can be changed later."
/>
</EuiText>
<EuiSpacer size="xxl" />
<EuiSelectable
options={selectionOptions}
singleSelection="always"
isLoading={loading}
height={100}
listProps={{ bordered: true, singleSelection: true }}
onChange={handleSelectableOnChange}
data-test-subj="onboardingPolicySelect"
<EuiSpacer size="xl" />
<EuiCallOut
color="primary"
iconType="iInCircle"
title={i18n.translate(
'xpack.securitySolution.endpoint.list.notAddedIntegrations.title',
{
defaultMessage: 'Integrations not added to an Agent policy',
}
)}
data-test-subj="integrationsNotAddedToAgentPolicyCallout"
>
{(list) => {
return loading ? (
<EuiSelectableMessage>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.loadingPolicies"
defaultMessage="Loading integrations"
/>
</EuiSelectableMessage>
) : selectionOptions.length ? (
list
) : (
<FormattedMessage
id="xpack.securitySolution.endpoint.list.noPolicies"
defaultMessage="There are no integrations."
/>
);
}}
</EuiSelectable>
<EuiSpacer size="s" />

<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.list.notAddedIntegrations.description"
defaultMessage="The following Elastic Defend integrations aren't added to an Agent policy, so they can't be deployed to an Agent. Click on an integration to edit it, and add it to an Agent policy:"
/>

<EuiSpacer size="s" />

<ul>
{policyItemsWithoutAgentPolicy.map((policyItem) => (
<li key={policyItem.id}>
<EuiLink
target="_blank"
href={getAppUrl({
appId: INTEGRATIONS_PLUGIN_ID,
path: pagePathGetters.integration_policy_edit({
packagePolicyId: policyItem.id,
})[1],
})}
data-test-subj="integrationWithoutAgentPolicyListItem"
>
{policyItem.name}
</EuiLink>
</li>
))}
</ul>

<FormattedMessage
id="xpack.securitySolution.endpoint.list.notAddedIntegrations.visitIntegrations"
defaultMessage="You can also view a list of all {integrationPolicies}."
values={{
integrationPolicies: (
<EuiLink
target="_blank"
href={getAppUrl({
appId: INTEGRATIONS_PLUGIN_ID,
path: pagePathGetters.integration_details_policies({
pkgkey: 'endpoint',
})[1],
})}
>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.notAddedIntegrations.integrationPolicies"
defaultMessage="Elastic Defend integration policies"
/>
</EuiLink>
),
}}
/>
</EuiText>
</EuiCallOut>
</>
),
},
{
title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', {
defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet',
}),
status: actionDisabled ? 'disabled' : '',
children: (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
[getAppUrl, policyItemsWithoutAgentPolicy]
);

const policySteps = useMemo(
() => [
{
title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', {
defaultMessage: 'Select the integration you want to use',
}),
children: (
<>
<EuiText color="subdued" size="m" grow={false}>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.stepTwo"
defaultMessage="You’ll be provided with the necessary commands to get started."
id="xpack.securitySolution.endpoint.list.stepOne"
defaultMessage="Select from existing integrations. This can be changed later."
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={onActionClick}
isDisabled={actionDisabled}
data-test-subj="onboardingStartButton"
<EuiSpacer size="xxl" />
<EuiSelectable
options={selectionOptions}
singleSelection="always"
isLoading={loading}
listProps={{ bordered: true, singleSelection: true }}
onChange={handleSelectableOnChange}
data-test-subj="onboardingPolicySelect"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
defaultMessage="Enroll Agent"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
),
},
],
[selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick]
);
{(list) => {
if (loading) {
return (
<EuiSelectableMessage>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.loadingPolicies"
defaultMessage="Loading integrations"
/>
</EuiSelectableMessage>
);
}

return (
<ManagementEmptyState
loading={loading}
dataTestSubj="emptyHostsTable"
steps={policySteps}
headerComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.list.noEndpointsPrompt"
defaultMessage="Next step: Enroll an Agent with Elastic Defend"
/>
}
bodyComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.list.noEndpointsInstructions"
defaultMessage="You’ve added the Elastic Defend integration. Now enroll your agents using the steps below."
/>
}
/>
);
});
if (!selectionOptions.length) {
return (
<EuiCallOut
color="warning"
data-test-subj="noIntegrationsAddedToAgentPoliciesCallout"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.noPoliciesAssignedToAgentPolicies"
defaultMessage="There are no Elastic Defend integrations added to Agent policies. To deploy Elastic Defend, add it to an Agent policy."
/>
</EuiCallOut>
);
}

return list;
}}
</EuiSelectable>

{policiesNotAddedToAgentPolicyCallout}
</>
),
},
{
title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', {
defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet',
}),
status: actionDisabled ? 'disabled' : '',
children: (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText color="subdued" size="m" grow={false}>
<FormattedMessage
id="xpack.securitySolution.endpoint.list.stepTwo"
defaultMessage="You'll be provided with the necessary commands to get started."
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={onActionClick}
isDisabled={actionDisabled}
data-test-subj="onboardingStartButton"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
defaultMessage="Enroll Agent"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
),
},
],
[
selectionOptions,
loading,
handleSelectableOnChange,
policiesNotAddedToAgentPolicyCallout,
actionDisabled,
onActionClick,
]
);

return (
<ManagementEmptyState
loading={loading}
dataTestSubj="emptyHostsTable"
steps={policySteps}
headerComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.list.noEndpointsPrompt"
defaultMessage="Next step: Enroll an Agent with Elastic Defend"
/>
}
bodyComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.list.noEndpointsInstructions"
defaultMessage="You've added the Elastic Defend integration. Now enroll your agents using the steps below."
/>
}
/>
);
}
);

const ManagementEmptyState = React.memo<{
loading: boolean;
Expand All @@ -284,7 +397,11 @@ const ManagementEmptyState = React.memo<{
{loading ? (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" className="essentialAnimation" />
<EuiLoadingSpinner
size="xl"
className="essentialAnimation"
data-test-subj="management-empty-state-loading-spinner"
/>
</EuiFlexItem>
</EuiFlexGroup>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,6 @@ const getAgentAndPoliciesForEndpointsList = async (
return;
}

// We use the Agent Policy API here, instead of the Package Policy, because we can't use
// filter by ID of the Saved Object. Agent Policy, however, keeps a reference (array) of
// Package Ids that it uses, thus if a reference exists there, then the package policy (policy)
// exists.
const policiesFound = (
await sendBulkGetPackagePolicies(http, policyIdsToCheck)
).items.reduce<PolicyIds>(
Expand Down
Loading

0 comments on commit 8eceb0d

Please sign in to comment.