Add 'inventory' item to Security navigation menu (#204373)

## Summary

Add 'inventory' item to Security navigation menu (either when the
sidebar is expanded and the full navigation menu is shown or when the
sidebar is collapsed and only the Security menu is visible).

### Changeset details

- Render 'inventory' item and enable `/app/security/asset_inventory`
route both conditionally based on feature flag
- Async loading/rendering of AssetInventory main page from within
SecuritySolution plugin
  - Delete unnecessary boilerplate existing in AssetInventory

### Out of scope
- AssetInventory nav sub-menu is skipped until more concrete
requirements are defined on what to do with them

### How to test

Activate the feature flag by adding this line to your local
`kibana.dev.yml`:

```yml
xpack.securitySolution.enableExperimental: ['assetInventoryStoreEnabled']
```

### Screenshots

<details><summary>Full menu (expanded mode)</summary>
<img width="240" alt="Screenshot 2024-12-16 at 13 12 45"
src="https://github.com/user-attachments/assets/f0939f38-5be6-481b-ace1-07f46f3622ae"
/>
</details> 

<details><summary>Only Security menu (collapsed mode)</summary>
<img width="256" alt="Screenshot 2024-12-16 at 13 12 33"
src="https://github.com/user-attachments/assets/b0bd62f0-5cea-4b7b-a731-3a53be362192"
/>
</details> 

<details><summary>AssetInventory loaded async from within Security
Solution</summary>
<img width="1640" alt="Screenshot 2024-12-16 at 17 23 01"
src="https://github.com/user-attachments/assets/b84716c9-6b18-4225-bf71-62c8ef07b302"
/>
</details> 

### Checklist

- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [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)

### Risks

No risks. Navigation item will be added if and only if feature flag is
enabled, which shouldn't happen for end users until development is
completed.
This commit is contained in:
Alberto Blázquez 2024-12-23 20:05:17 +01:00 committed by GitHub
parent 71303af255
commit 6d5be7461b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 191 additions and 68 deletions

View file

@ -11,6 +11,7 @@ export enum SecurityPageName {
administration = 'administration',
alerts = 'alerts',
assets = 'assets',
assetInventory = 'asset_inventory',
attackDiscovery = 'attack_discovery',
blocklist = 'blocklist',
/*

View file

@ -1,24 +0,0 @@
/*
* 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.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import type { AppPluginStartDependencies } from './types';
import { AssetInventoryApp } from './components/app';
export const renderApp = (
{ notifications, http }: CoreStart,
{}: AppPluginStartDependencies,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<AssetInventoryApp basename={appBasePath} notifications={notifications} http={http} />,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -6,33 +6,26 @@
*/
import React from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { BrowserRouter as Router } from '@kbn/shared-ux-router';
import { EuiPageTemplate, EuiTitle } from '@elastic/eui';
import type { CoreStart } from '@kbn/core/public';
interface AssetInventoryAppDeps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
}
export const AssetInventoryApp = ({ basename }: AssetInventoryAppDeps) => {
const AssetInventoryApp = () => {
return (
<Router basename={basename}>
<I18nProvider>
<>
<EuiPageTemplate restrictWidth="1000px">
<EuiPageTemplate.Header>
<EuiTitle size="l">
<h1>
<FormattedMessage id="assetInventory.helloWorldText" defaultMessage="Inventory" />
</h1>
</EuiTitle>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section />
</EuiPageTemplate>
</>
</I18nProvider>
</Router>
<I18nProvider>
<>
<EuiPageTemplate restrictWidth="1000px">
<EuiPageTemplate.Header>
<EuiTitle size="l">
<h1>
<FormattedMessage id="assetInventory.allAssets" defaultMessage="All Assets" />
</h1>
</EuiTitle>
</EuiPageTemplate.Header>
<EuiPageTemplate.Section />
</EuiPageTemplate>
</>
</I18nProvider>
);
};
// we need to use default exports to import it via React.lazy
export default AssetInventoryApp; // eslint-disable-line import/no-default-export

View file

@ -0,0 +1,34 @@
/*
* 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.
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppPluginStartDependencies } from '../types';
// Initializing react-query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
},
},
});
const AssetInventoryLazy = lazy(() => import('../components/app'));
export const getAssetInventoryLazy = (props: AppPluginStartDependencies) => {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<EuiLoadingSpinner />}>
<AssetInventoryLazy {...props} />
</Suspense>
</QueryClientProvider>
);
};

View file

@ -4,12 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import type {
AssetInventoryPluginSetup,
AssetInventoryPluginStart,
AppPluginStartDependencies,
} from './types';
import { getAssetInventoryLazy } from './methods';
export class AssetInventoryPlugin
implements Plugin<AssetInventoryPluginSetup, AssetInventoryPluginStart>
@ -17,16 +18,10 @@ export class AssetInventoryPlugin
public setup(core: CoreSetup): AssetInventoryPluginSetup {
return {};
}
public start(
coreStart: CoreStart,
depsStart: AppPluginStartDependencies
): AssetInventoryPluginStart {
public start(coreStart: CoreStart): AssetInventoryPluginStart {
return {
getAssetInventoryPage: async (params: AppMountParameters) => {
// Load application bundle
const { renderApp } = await import('./application');
// Render the application
return renderApp(coreStart, depsStart as AppPluginStartDependencies, params);
getAssetInventoryPage: (assetInventoryDeps: AppPluginStartDependencies) => {
return getAssetInventoryLazy(assetInventoryDeps);
},
};
}

View file

@ -8,8 +8,9 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AssetInventoryPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AssetInventoryPluginStart {}
export interface AssetInventoryPluginStart {
getAssetInventoryPage: (assetInventoryStartDeps: AppPluginStartDependencies) => JSX.Element;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AppPluginStartDependencies {}

View file

@ -14,10 +14,5 @@
"../../../../../typings/**/*"
],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/i18n-react",
"@kbn/shared-ux-router",
"@kbn/securitysolution-es-utils"
]
"kbn_references": ["@kbn/core", "@kbn/i18n-react", "@kbn/securitysolution-es-utils"]
}

View file

@ -19,6 +19,7 @@ export { SecurityPageName } from '@kbn/security-solution-navigation';
*/
export const APP_ID = 'securitySolution' as const;
export const APP_UI_ID = 'securitySolutionUI' as const;
export const ASSET_INVENTORY_FEATURE_ID = 'securitySolutionAssetInventory' as const;
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
export const CASES_FEATURE_ID = 'securitySolutionCasesV2' as const;
@ -102,6 +103,7 @@ export const EXCEPTIONS_PATH = '/exceptions' as const;
export const EXCEPTION_LIST_DETAIL_PATH = `${EXCEPTIONS_PATH}/details/:detailName` as const;
export const HOSTS_PATH = '/hosts' as const;
export const ATTACK_DISCOVERY_PATH = '/attack_discovery' as const;
export const ASSET_INVENTORY_PATH = '/asset_inventory' as const;
export const USERS_PATH = '/users' as const;
export const KUBERNETES_PATH = '/kubernetes' as const;
export const NETWORK_PATH = '/network' as const;

View file

@ -16,6 +16,7 @@
],
"requiredPlugins": [
"actions",
"assetInventory",
"alerting",
"cases",
"cloud",

View file

@ -37,6 +37,10 @@ export const CATEGORIES: Array<SeparatorLinkCategory<SolutionPageName>> = [
SecurityPageName.exploreLanding,
],
},
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.assetInventory],
},
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.assets],

View file

@ -119,6 +119,10 @@ export const ATTACK_DISCOVERY = i18n.translate(
}
);
export const INVENTORY = i18n.translate('xpack.securitySolution.navigation.inventory', {
defaultMessage: 'Inventory',
});
export const TIMELINES = i18n.translate('xpack.securitySolution.navigation.timelines', {
defaultMessage: 'Timelines',
});

View file

@ -7,6 +7,7 @@
import type { CoreStart } from '@kbn/core/public';
import { links as attackDiscoveryLinks } from './attack_discovery/links';
import { links as assetInventoryLinks } from './asset_inventory/links';
import type { AppLinkItems } from './common/links/types';
import { indicatorsLinks } from './threat_intelligence/links';
import { links as alertsLinks } from './detections/links';
@ -32,6 +33,7 @@ export const appLinks: AppLinkItems = Object.freeze([
timelinesLinks,
indicatorsLinks,
exploreLinks,
assetInventoryLinks,
rulesLinks,
onboardingLinks,
managementLinks,
@ -52,6 +54,7 @@ export const getFilteredLinks = async (
timelinesLinks,
indicatorsLinks,
exploreLinks,
assetInventoryLinks,
rulesLinks,
onboardingLinks,
managementFilteredLinks,

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
import type { SecuritySubPlugin } from '../app/types';
import { routes } from './routes';
export class AssetInventory {
public setup() {}
public start(): SecuritySubPlugin {
return {
routes,
};
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.
*/
import { i18n } from '@kbn/i18n';
import { INVENTORY } from '../app/translations';
import { ASSET_INVENTORY_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants';
import type { LinkItem } from '../common/links/types';
export const links: LinkItem = {
capabilities: [`${SERVER_APP_ID}.show`],
globalNavPosition: 10,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.inventory', {
defaultMessage: 'Inventory',
}),
],
experimentalKey: 'assetInventoryStoreEnabled',
id: SecurityPageName.assetInventory,
path: ASSET_INVENTORY_PATH,
title: INVENTORY,
};

View file

@ -0,0 +1,25 @@
/*
* 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.
*/
import React from 'react';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useKibana } from '../../common/lib/kibana';
import { SecurityPageName } from '../../../common/constants';
import { SpyRoute } from '../../common/utils/route/spy_routes';
export const AssetInventoryContainer = React.memo(() => {
const { assetInventory } = useKibana().services;
return (
<SecuritySolutionPageWrapper noPadding>
{assetInventory.getAssetInventoryPage({})}
<SpyRoute pageName={SecurityPageName.assetInventory} />
</SecuritySolutionPageWrapper>
);
});
AssetInventoryContainer.displayName = 'AssetInventoryContainer';

View file

@ -0,0 +1,30 @@
/*
* 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.
*/
import React from 'react';
import type { SecuritySubPluginRoutes } from '../app/types';
import { SecurityPageName } from '../app/types';
import { ASSET_INVENTORY_PATH } from '../../common/constants';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
import { ExperimentalFeaturesService } from '../common/experimental_features_service';
import { AssetInventoryContainer } from './pages';
export const AssetInventoryRoutes = () => (
<PluginTemplateWrapper>
<SecurityRoutePageWrapper pageName={SecurityPageName.assetInventory}>
<AssetInventoryContainer />
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);
export const routes: SecuritySubPluginRoutes = [
{
path: ExperimentalFeaturesService.get().assetInventoryStoreEnabled ? ASSET_INVENTORY_PATH : [],
component: AssetInventoryRoutes,
},
];

View file

@ -31,4 +31,8 @@ export const CATEGORIES: SeparatorLinkCategory[] = [
SecurityPageName.exploreLanding,
],
},
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.assetInventory],
},
];

View file

@ -10,6 +10,7 @@
* By loading these later we can reduce the initial bundle size and allow users to delay loading these dependencies until they are needed.
*/
import { AssetInventory } from './asset_inventory';
import { AttackDiscovery } from './attack_discovery';
import { Cases } from './cases';
import { Detections } from './detections';
@ -35,6 +36,7 @@ import { SiemMigrations } from './siem_migrations';
* The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import.
*/
const subPluginClasses = {
AssetInventory,
AttackDiscovery,
Detections,
Cases,

View file

@ -99,7 +99,7 @@ export const links: LinkItem = {
path: MANAGE_PATH,
skipUrlState: true,
hideTimeline: true,
globalNavPosition: 10,
globalNavPosition: 11,
capabilities: [`${SERVER_APP_ID}.show`],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.manage', {

View file

@ -288,6 +288,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const { subPluginClasses } = await this.lazySubPlugins();
this._subPlugins = {
alerts: new subPluginClasses.Detections(),
assetInventory: new subPluginClasses.AssetInventory(),
attackDiscovery: new subPluginClasses.AttackDiscovery(),
rules: new subPluginClasses.Rules(),
exceptions: new subPluginClasses.Exceptions(),
@ -321,6 +322,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const alerts = await subPlugins.alerts.start(storage, plugins);
return {
alerts,
assetInventory: subPlugins.assetInventory.start(),
attackDiscovery: subPlugins.attackDiscovery.start(),
cases: subPlugins.cases.start(),
cloudDefend: subPlugins.cloudDefend.start(),

View file

@ -49,6 +49,7 @@ import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { AssetInventoryPluginStart } from '@kbn/asset-inventory-plugin/public';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { ManagementSetup } from '@kbn/management-plugin/public';
@ -77,6 +78,7 @@ import type { CloudSecurityPosture } from './cloud_security_posture';
import type { CloudDefend } from './cloud_defend';
import type { ThreatIntelligence } from './threat_intelligence';
import type { SecuritySolutionTemplateWrapper } from './app/home/template_wrapper';
import type { AssetInventory } from './asset_inventory';
import type { AttackDiscovery } from './attack_discovery';
import type { Explore } from './explore';
import type { NavigationLink } from './common/links';
@ -121,6 +123,7 @@ export interface SetupPlugins {
* in the code.
*/
export interface StartPlugins {
assetInventory: AssetInventoryPluginStart;
cases: CasesPublicStart;
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
@ -231,6 +234,7 @@ export const CASES_SUB_PLUGIN_KEY = 'cases';
export interface SubPlugins {
[CASES_SUB_PLUGIN_KEY]: Cases;
alerts: Detections;
assetInventory: AssetInventory;
attackDiscovery: AttackDiscovery;
cloudDefend: CloudDefend;
cloudSecurityPosture: CloudSecurityPosture;
@ -255,6 +259,7 @@ export interface SubPlugins {
export interface StartedSubPlugins {
[CASES_SUB_PLUGIN_KEY]: ReturnType<Cases['start']>;
alerts: Awaited<ReturnType<Detections['start']>>;
assetInventory: Awaited<ReturnType<AssetInventory['start']>>;
attackDiscovery: ReturnType<AttackDiscovery['start']>;
cloudDefend: ReturnType<CloudDefend['start']>;
cloudSecurityPosture: ReturnType<CloudSecurityPosture['start']>;

View file

@ -215,6 +215,7 @@
"@kbn/cbor",
"@kbn/zod",
"@kbn/cloud-security-posture",
"@kbn/asset-inventory-plugin",
"@kbn/security-solution-distribution-bar",
"@kbn/cloud-security-posture-common",
"@kbn/cloud-security-posture-graph",