mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
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:
parent
71303af255
commit
6d5be7461b
22 changed files with 191 additions and 68 deletions
|
@ -11,6 +11,7 @@ export enum SecurityPageName {
|
|||
administration = 'administration',
|
||||
alerts = 'alerts',
|
||||
assets = 'assets',
|
||||
assetInventory = 'asset_inventory',
|
||||
attackDiscovery = 'attack_discovery',
|
||||
blocklist = 'blocklist',
|
||||
/*
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
],
|
||||
"requiredPlugins": [
|
||||
"actions",
|
||||
"assetInventory",
|
||||
"alerting",
|
||||
"cases",
|
||||
"cloud",
|
||||
|
|
|
@ -37,6 +37,10 @@ export const CATEGORIES: Array<SeparatorLinkCategory<SolutionPageName>> = [
|
|||
SecurityPageName.exploreLanding,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.assetInventory],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.assets],
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -31,4 +31,8 @@ export const CATEGORIES: SeparatorLinkCategory[] = [
|
|||
SecurityPageName.exploreLanding,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: LinkCategoryType.separator,
|
||||
linkIds: [SecurityPageName.assetInventory],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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']>;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue