[Security Solution] Use static declaration for navigation hierarchy (#215969)

## Summary

Part of Epic: https://github.com/elastic/kibana-team/issues/1439
Addresses https://github.com/elastic/kibana/issues/212903, but does not
remove the landing page access. The landing page access will be removed
in https://github.com/elastic/kibana/pull/210893

**Changes**
1. Converts the declaration of the Security Solution side navigation for
serverless and stateful projects into a static declaration, rather than
algorithmically parsing registered links to dynamically build the
declaration.
2. Updates the contents of the "Assets" panel to prepare for removal of
that landing page.
3. Eliminates the top-level nesting of the nav items, which removes the
extra space between the project title and the first nav items. See
45454bdc4d

**Known issue**: Clicking the "Browse integrations" button does not
close the secondary nav panel. Doing that will be a relatively simple
chore, but will require some changes in the SharedUX chrome-navigation
package, as well as the `LinkButton` component in the Security Solution
navigation-links package.

### Screenshots
Serverless

![static-nav-declaration-security-serverless-3iorteikghiskhgkseh](https://github.com/user-attachments/assets/47bcbea9-7e3c-481e-b1b8-4b13bb5d63b1)

Stateful/ECH

![static-nav-declaration-security-stateful-3iorteikghiskhgkseh](https://github.com/user-attachments/assets/3d3c8a0e-95d1-4da7-a657-c824577b6ec1)

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [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/src/platform/packages/shared/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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2025-04-03 08:38:04 -07:00 committed by GitHub
parent 5a3c2c0f05
commit ef907a32f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 3958 additions and 3478 deletions

View file

@ -150,7 +150,7 @@ pageLoadAssetSize:
searchSynonyms: 20262
security: 81771
securitySolution: 98429
securitySolutionEss: 31781
securitySolutionEss: 36000
securitySolutionServerless: 62488
serverless: 16573
serverlessChat: 22715

View file

@ -27,6 +27,7 @@ export enum SecurityPageName {
*/
cloudSecurityPostureBenchmarks = 'cloud_security_posture-benchmarks',
cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard',
cloudSecurityPostureVulnerabilityDashboard = 'cloud_security_posture-vulnerability_dashboard',
cloudSecurityPostureFindings = 'cloud_security_posture-findings',
cloudSecurityPostureRules = 'cloud_security_posture-rules',
dashboards = 'dashboards',

View file

@ -41965,13 +41965,6 @@
"xpack.securitySolutionServerless.endpointProtectionUpdates.cardMessage": "Pour modifier les mises à jour de protection, vous devez ajouter au moins Endpoint Complete à votre projet.",
"xpack.securitySolutionServerless.endpointProtectionUpdates.cardTitle": "Mises à jour de la protection",
"xpack.securitySolutionServerless.entityStoreEnablementCallout.additionalChargesMessage": "Veuillez noter que l'activation de ces fonctionnalités peut entraîner des frais supplémentaires en fonction de votre formule d'abonnement. Examinez attentivement les détails de votre abonnement avant de procéder afin d'éviter des coûts inattendus.",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.access": "Accès",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.alertsAndInsights": "Alertes et informations exploitables",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.content": "Contenu",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.data": "Données",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.other": "Autre",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.usersAndRoles": "Gérer les membres de l'organisation",
"xpack.securitySolutionServerless.navLinks.projectSettings.title": "Paramètres de projet",
"xpack.securitySolutionServerless.osquery.paywall.body": "Passez votre licence au niveau {productTypeRequired} pour utiliser les actions de réponse Osquery.",
"xpack.securitySolutionServerless.osquery.paywall.title": "Toujours plus avec Security !",
"xpack.securitySolutionServerless.rules.endpointSecurity.agentTamperProtection.badgeText": "Endpoint Complete",

View file

@ -41937,13 +41937,6 @@
"xpack.securitySolutionServerless.endpointProtectionUpdates.cardMessage": "保護更新を変更するには、少なくともエンドポイント完了をプロジェクトに追加する必要があります。",
"xpack.securitySolutionServerless.endpointProtectionUpdates.cardTitle": "保護更新",
"xpack.securitySolutionServerless.entityStoreEnablementCallout.additionalChargesMessage": "これらの機能を有効にすると、サブスクリプションプランによっては追加料金が発生する場合があります。想定外の費用を避けるため、続行する前に、プランの詳細を十分に確認してください。",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.access": "アクセス",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.alertsAndInsights": "アラートとインサイト",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.content": "コンテンツ",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.data": "データ",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.other": "Other",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.usersAndRoles": "組織メンバーを管理",
"xpack.securitySolutionServerless.navLinks.projectSettings.title": "プロジェクト設定",
"xpack.securitySolutionServerless.osquery.paywall.body": "Osquery Response Actionsを使用するには、ライセンスを{productTypeRequired}にアップグレードしてください。",
"xpack.securitySolutionServerless.osquery.paywall.title": "Securityではさまざまなことが可能です",
"xpack.securitySolutionServerless.rules.endpointSecurity.agentTamperProtection.badgeText": "エンドポイント完了",

View file

@ -42003,13 +42003,6 @@
"xpack.securitySolutionServerless.endpointProtectionUpdates.cardMessage": "要修改防护更新,必须至少将 Endpoint Complete 添加到您的项目。",
"xpack.securitySolutionServerless.endpointProtectionUpdates.cardTitle": "防护更新",
"xpack.securitySolutionServerless.entityStoreEnablementCallout.additionalChargesMessage": "请注意,激活这些功能可能会产生其他费用,具体取决于您的订阅计划。请仔细复查您的计划详情,以避免出现意外成本,然后继续。",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.access": "访问",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.alertsAndInsights": "告警和洞见",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.content": "内容",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.data": "数据",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.other": "其他",
"xpack.securitySolutionServerless.navLinks.projectSettings.mngt.usersAndRoles": "管理组织成员",
"xpack.securitySolutionServerless.navLinks.projectSettings.title": "项目设置",
"xpack.securitySolutionServerless.osquery.paywall.body": "将您的许可证升级到{productTypeRequired}以使用 Osquery 响应操作。",
"xpack.securitySolutionServerless.osquery.paywall.title": "Security 让您事半功倍!",
"xpack.securitySolutionServerless.rules.endpointSecurity.agentTamperProtection.badgeText": "Endpoint Complete",

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export { i18nStrings } from './src/i18n_strings';
export {
useGetLinkUrl,
useGetLinkProps,
@ -12,5 +13,6 @@ export {
LinkButton,
LinkAnchor,
isSecurityId,
securityLink,
} from './src/links';
export type { GetLinkUrl, GetLinkProps, LinkProps } from './src/links';

View file

@ -0,0 +1,239 @@
/*
* 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';
export const i18nStrings = {
rules: {
title: i18n.translate('securitySolutionPackages.navLinks.rules', {
defaultMessage: 'Rules',
}),
management: {
title: i18n.translate('securitySolutionPackages.navLinks.rules.management', {
defaultMessage: 'Management',
}),
siemMigrationsRules: i18n.translate(
'securitySolutionPackages.navLinks.rules.management.siemRuleMigrations',
{ defaultMessage: 'SIEM rule migrations' }
),
discover: i18n.translate('securitySolutionPackages.navLinks.rules.discover', {
defaultMessage: 'Discover',
}),
},
},
investigations: {
title: i18n.translate('securitySolutionPackages.navLinks.investigations', {
defaultMessage: 'Investigations',
}),
},
explore: {
title: i18n.translate('securitySolutionPackages.navLinks.explore', {
defaultMessage: 'Explore',
}),
},
assets: {
title: i18n.translate('securitySolutionPackages.navLinks.assets', {
defaultMessage: 'Assets',
}),
fleet: {
title: i18n.translate('securitySolutionPackages.navLinks.assets.fleet', {
defaultMessage: 'Fleet',
}),
policies: i18n.translate('securitySolutionPackages.navLinks.assets.fleetPolicies', {
defaultMessage: 'Policies',
}),
},
endpoints: {
title: i18n.translate('securitySolutionPackages.navLinks.assets.endpoints', {
defaultMessage: 'Endpoints',
}),
},
integrationsCallout: {
title: i18n.translate('securitySolutionPackages.navLinks.assets.integrationsCallout.title', {
defaultMessage: 'Integrations',
}),
body: i18n.translate('securitySolutionPackages.navLinks.assets.integrationsCallout.body', {
defaultMessage: 'Choose an integration to start collecting and analyzing your data.',
}),
button: i18n.translate(
'securitySolutionPackages.navLinks.assets.integrationsCallout.button',
{ defaultMessage: 'Browse integrations' }
),
},
},
ml: {
title: i18n.translate('securitySolutionPackages.navLinks.ml', {
defaultMessage: 'Machine learning',
}),
overview: i18n.translate('securitySolutionPackages.navLinks.ml.overview', {
defaultMessage: 'Overview',
}),
notifications: i18n.translate('securitySolutionPackages.navLinks.ml.notifications', {
defaultMessage: 'Notifications',
}),
memoryUsage: i18n.translate('securitySolutionPackages.navLinks.ml.memoryUsage', {
defaultMessage: 'Memory usage',
}),
anomalyDetection: {
title: i18n.translate('securitySolutionPackages.navLinks.ml.anomalyDetection', {
defaultMessage: 'Anomaly detection',
}),
jobs: i18n.translate('securitySolutionPackages.navLinks.ml.anomalyDetection.jobs', {
defaultMessage: 'Jobs',
}),
anomalyExplorer: i18n.translate(
'securitySolutionPackages.navLinks.ml.anomalyDetection.anomalyExplorer',
{ defaultMessage: 'Anomaly explorer' }
),
singleMetricViewer: i18n.translate(
'securitySolutionPackages.navLinks.ml.anomalyDetection.singleMetricViewer',
{ defaultMessage: 'Single metric viewer' }
),
suppliedConfigurations: i18n.translate(
'securitySolutionPackages.navLinks.ml.anomalyDetection.suppliedConfigurations',
{ defaultMessage: 'Supplied configurations' }
),
settings: i18n.translate('securitySolutionPackages.navLinks.ml.anomalyDetection.settings', {
defaultMessage: 'Settings',
}),
},
dataFrameAnalytics: {
title: i18n.translate('securitySolutionPackages.navLinks.ml.dataFrameAnalytics', {
defaultMessage: 'Data frame analytics',
}),
jobs: i18n.translate('securitySolutionPackages.navLinks.ml.dataFrameAnalytics.jobs', {
defaultMessage: 'Jobs',
}),
resultExplorer: i18n.translate(
'securitySolutionPackages.navLinks.ml.dataFrameAnalytics.resultExplorer',
{ defaultMessage: 'Result explorer' }
),
analyticsMap: i18n.translate(
'securitySolutionPackages.navLinks.ml.dataFrameAnalytics.analyticsMap',
{ defaultMessage: 'Analytics map' }
),
},
modelManagement: {
title: i18n.translate('securitySolutionPackages.navLinks.ml.modelManagement', {
defaultMessage: 'Model management',
}),
trainedModels: i18n.translate(
'securitySolutionPackages.navLinks.ml.modelManagement.trainedModels',
{ defaultMessage: 'Trained models' }
),
},
dataVisualizer: {
title: i18n.translate('securitySolutionPackages.navLinks.ml.dataVisualizer', {
defaultMessage: 'Data visualizer',
}),
fileDataVisualizer: i18n.translate(
'securitySolutionPackages.navLinks.ml.dataVisualizer.fileDataVisualizer',
{ defaultMessage: 'File data visualizer' }
),
dataViewDataVisualizer: i18n.translate(
'securitySolutionPackages.navLinks.ml.dataVisualizer.dataViewDataVisualizer',
{ defaultMessage: 'Data view data visualizer' }
),
esqlDataVisualizer: i18n.translate(
'securitySolutionPackages.navLinks.ml.dataVisualizer.esqlDataVisualizer',
{ defaultMessage: 'ES|QL data visualizer' }
),
dataDrift: i18n.translate('securitySolutionPackages.navLinks.ml.dataVisualizer.dataDrift', {
defaultMessage: 'Data drift',
}),
},
aiopsLabs: {
title: i18n.translate('securitySolutionPackages.navLinks.ml.aiopsLabs', {
defaultMessage: 'Aiops labs',
}),
logRateAnalysis: i18n.translate(
'securitySolutionPackages.navLinks.ml.aiopsLabs.logRateAnalysis',
{ defaultMessage: 'Log rate analysis' }
),
logPatternAnalysis: i18n.translate(
'securitySolutionPackages.navLinks.ml.aiopsLabs.logPatternAnalysis',
{ defaultMessage: 'Log pattern analysis' }
),
changePointDetection: i18n.translate(
'securitySolutionPackages.navLinks.ml.aiopsLabs.changePointDetection',
{ defaultMessage: 'Change point detection' }
),
},
},
entityRiskScore: i18n.translate('securitySolutionPackages.navLinks.entityRiskScore', {
defaultMessage: 'Entity Risk Score',
}),
entityStore: i18n.translate('securitySolutionPackages.navLinks.entityStore', {
defaultMessage: 'Entity Store',
}),
devTools: i18n.translate('securitySolutionPackages.navLinks.devTools', {
defaultMessage: 'Developer tools',
}),
management: {
title: i18n.translate('securitySolutionPackages.navLinks.management.title', {
defaultMessage: 'Management',
}),
},
projectSettings: {
title: i18n.translate('securitySolutionPackages.navLinks.projectSettings.title', {
defaultMessage: 'Project Settings',
}),
},
stackManagement: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.title', {
defaultMessage: 'Stack Management',
}),
ingest: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.ingest', {
defaultMessage: 'Ingest',
}),
},
data: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.data', {
defaultMessage: 'Data',
}),
},
access: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.access', {
defaultMessage: 'Access',
}),
usersAndRoles: i18n.translate('securitySolutionPackages.navLinks.mngt.usersAndRoles', {
defaultMessage: 'Manage organization members',
}),
},
alertsAndInsights: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.alertsAndInsights', {
defaultMessage: 'Alerts and Insights',
}),
},
security: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.security', {
defaultMessage: 'Security',
}),
},
kibana: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.kibana', {
defaultMessage: 'Kibana',
}),
},
content: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.content', {
defaultMessage: 'Content',
}),
},
stack: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.stack', {
defaultMessage: 'Stack',
}),
},
other: {
title: i18n.translate('securitySolutionPackages.navLinks.mngt.other', {
defaultMessage: 'Other',
}),
},
},
};

View file

@ -8,6 +8,8 @@
import type { HTMLAttributeAnchorTarget } from 'react';
import React, { type MouseEventHandler, type MouseEvent, useCallback } from 'react';
import { EuiButton, EuiLink, type EuiLinkProps } from '@elastic/eui';
import type { SecurityPageName } from '@kbn/deeplinks-security';
import type { AppDeepLinkId } from '@kbn/core-chrome-browser';
import { useGetAppUrl, useNavigateTo } from './navigation';
export interface BaseLinkProps {
@ -170,3 +172,7 @@ export const formatPath = (path: string, urlState: string) => {
export const isModified = (event: MouseEvent) =>
event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
export const securityLink = (pageName: SecurityPageName): AppDeepLinkId => {
return `securitySolutionUI:${pageName}`;
};

View file

@ -15,7 +15,8 @@
"kbn_references": [
"@kbn/i18n",
"@kbn/core",
"@kbn/deeplinks-security"
"@kbn/deeplinks-security",
"@kbn/core-chrome-browser"
],
"exclude": ["target/**/*"]
}

View file

@ -1,8 +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.
*/
export { getSolutionNavigation } from './solution_navigation';

View file

@ -1,207 +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 { partition } from 'lodash/fp';
import type {
AppDeepLinkId,
NodeDefinition,
NavigationTreeDefinition,
RootNavigationItemDefinition,
} from '@kbn/core-chrome-browser';
import type { LinkCategory } from '@kbn/security-solution-navigation';
import {
isSeparatorLinkCategory,
isTitleLinkCategory,
isAccordionLinkCategory,
} from '@kbn/security-solution-navigation';
import type { SolutionPageName, SolutionLinkCategory, SolutionNavLink } from '../../common/links';
import {
getNavLinkIdFromSolutionPageName,
isBreadcrumbHidden,
isSideNavStatusHidden,
} from './util';
import { SOLUTION_NAME } from '../../common/translations';
export const formatNavigationTree = (
solutionNavLinks: SolutionNavLink[],
bodyCategories: Readonly<SolutionLinkCategory[]>,
footerCategories: Readonly<SolutionLinkCategory[]>
): NavigationTreeDefinition => {
const [footerNavItems, bodyNavItems] = partition('isFooterLink', solutionNavLinks);
const bodyChildren = addMainLinksPanelOpenerProp(
formatNodesFromLinks(bodyNavItems, bodyCategories, [])
);
return {
body: [
{
type: 'navGroup',
id: 'security_solution_nav',
title: SOLUTION_NAME,
icon: 'logoSecurity',
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
children: bodyChildren,
isCollapsible: false,
},
],
footer: formatFooterNodesFromLinks(footerNavItems, footerCategories),
};
};
// Body
const formatNodesFromLinks = (
solutionNavLinks: SolutionNavLink[],
parentCategories: Readonly<Array<LinkCategory<SolutionPageName>>>,
ids: SolutionPageName[]
): NodeDefinition[] => {
const nodes: NodeDefinition[] = [];
if (parentCategories?.length) {
parentCategories.forEach((category) => {
nodes.push(...formatNodesFromLinksWithCategory(solutionNavLinks, category, ids));
}, []);
} else {
nodes.push(...formatNodesFromLinksWithoutCategory(solutionNavLinks, ids));
}
return nodes;
};
const formatNodesFromLinksWithCategory = (
solutionNavLinks: SolutionNavLink[],
category: LinkCategory<SolutionPageName>,
ids: SolutionPageName[]
): NodeDefinition[] => {
if (!category?.linkIds) {
return [];
}
if (category.linkIds) {
const children = category.linkIds.reduce<NodeDefinition[]>((acc, linkId) => {
const solutionNavLink = solutionNavLinks.find(({ id }) => id === linkId);
if (solutionNavLink != null) {
acc.push(createNodeFromSolutionNavLink(solutionNavLink, ids));
}
return acc;
}, []);
if (!children.length) {
return [];
}
const id = isTitleLinkCategory(category) ? getCategoryIdFromLabel(category.label) : undefined;
return [
{
id,
...(isTitleLinkCategory(category) && { title: category.label }),
breadcrumbStatus: 'hidden',
children,
},
];
}
return [];
};
const formatNodesFromLinksWithoutCategory = (
solutionNavLinks: SolutionNavLink[],
ids: SolutionPageName[]
): NodeDefinition[] => {
return solutionNavLinks.map((solutionNavLink) =>
createNodeFromSolutionNavLink(solutionNavLink, ids)
);
};
const createNodeFromSolutionNavLink = (
solutionNavLink: SolutionNavLink,
ids: SolutionPageName[]
): NodeDefinition => {
const { id, title, links, categories, disabled } = solutionNavLink;
const link = getNavLinkIdFromSolutionPageName(id);
const node: NodeDefinition = {
id,
link: link as AppDeepLinkId,
title,
...(isBreadcrumbHidden(id) && { breadcrumbStatus: 'hidden' }),
...((isSideNavStatusHidden(ids) || disabled) && { sideNavStatus: 'hidden' }),
};
if (links?.length) {
node.children = formatNodesFromLinks(links, categories ?? [], ids.concat(id));
}
return node;
};
// Footer
const formatFooterNodesFromLinks = (
solutionNavLinks: SolutionNavLink[],
parentCategories?: Readonly<Array<LinkCategory<SolutionPageName>>>
): RootNavigationItemDefinition[] => {
const nodes: RootNavigationItemDefinition[] = [];
if (parentCategories?.length) {
parentCategories.forEach((category) => {
if (isSeparatorLinkCategory(category)) {
nodes.push(
...category.linkIds.reduce<RootNavigationItemDefinition[]>((acc, linkId) => {
const solutionNavLink = solutionNavLinks.find(({ id }) => id === linkId);
if (solutionNavLink != null) {
acc.push({
type: 'navItem',
link: getNavLinkIdFromSolutionPageName(solutionNavLink.id) as AppDeepLinkId,
title: solutionNavLink.title,
icon: solutionNavLink.sideNavIcon,
});
}
return acc;
}, [])
);
}
if (isAccordionLinkCategory(category)) {
nodes.push({
type: 'navGroup',
id: getCategoryIdFromLabel(category.label),
title: category.label,
icon: category.iconType,
breadcrumbStatus: 'hidden',
children:
category.linkIds?.reduce<NodeDefinition[]>((acc, linkId) => {
const solutionNavLink = solutionNavLinks.find(({ id }) => id === linkId);
if (solutionNavLink != null) {
acc.push({
title: solutionNavLink.title,
link: getNavLinkIdFromSolutionPageName(solutionNavLink.id) as AppDeepLinkId,
});
}
return acc;
}, []) ?? [],
});
}
}, []);
}
return nodes;
};
// Utils
const getCategoryIdFromLabel = (label: string): string =>
`category-${label.toLowerCase().replace(' ', '_')}`;
/**
* Adds the `renderAs: 'panelOpener'` prop to the main links that have children
* This function expects all main links to be in nested groups to add the separation between them.
* If these "separator" groups change this function will need to be updated.
*/
const addMainLinksPanelOpenerProp = (nodes: NodeDefinition[]): NodeDefinition[] =>
nodes.map((node): NodeDefinition => {
if (node.children?.length) {
return {
...node,
children: node.children.map((child) => ({
...child,
...(child.children && { renderAs: 'panelOpener' }),
})),
};
}
return node;
});

View file

@ -1,84 +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 type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import type { PanelComponentProps } from '@kbn/shared-ux-chrome-navigation/src/ui/components/panel/types';
import { SolutionSideNavPanelContent } from '@kbn/security-solution-side-nav/panel';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import { map } from 'rxjs';
import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { CoreStart } from '@kbn/core/public';
import { usePanelSideNavItems } from './use_panel_side_nav_items';
import { CATEGORIES, FOOTER_CATEGORIES } from './categories';
import { formatNavigationTree } from './navigation_tree';
import type { SolutionNavLinks$ } from '../../common/links';
import { navLinks$ } from '../../common/links/nav_links';
export const withNavigationProvider = <T extends object>(
Component: React.ComponentType<T>,
core: CoreStart
) =>
function WithNavigationProvider(props: T) {
return (
<NavigationProvider core={core}>
<Component {...props} />
</NavigationProvider>
);
};
const getPanelContent = (
core: CoreStart,
solutionNavLinks$: SolutionNavLinks$
): React.FC<PanelComponentProps> => {
const PanelContentProvider: React.FC<PanelComponentProps> = React.memo(
function PanelContentProvider({ selectedNode: { id: linkId }, closePanel }) {
const solutionNavLinks = useObservable(solutionNavLinks$, []);
const currentPanelItem = solutionNavLinks.find((item) => item.id === linkId);
const { title = '', links = [], categories } = currentPanelItem ?? {};
const items = usePanelSideNavItems(links);
if (items.length === 0) {
return null;
}
return (
<SolutionSideNavPanelContent
title={title}
items={items}
categories={categories}
onClose={closePanel}
/>
);
}
);
return withNavigationProvider(PanelContentProvider, core);
};
export interface SolutionNavigation {
navigationTree$: Observable<NavigationTreeDefinition>;
panelContentProvider: PanelContentProvider;
}
export const getSolutionNavigation = (core: CoreStart): SolutionNavigation => {
const panelContent = getPanelContent(core, navLinks$);
const panelContentProvider: PanelContentProvider = (id: string) => {
// Stack Management uses the default panel content
if (!id.endsWith('.stack_management')) {
return { content: panelContent };
}
};
const navigationTree$ = navLinks$.pipe(
map((solutionNavLinks) => formatNavigationTree(solutionNavLinks, CATEGORIES, FOOTER_CATEGORIES))
);
return { navigationTree$, panelContentProvider };
};

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
import { APP_UI_ID } from '../../../common';
import type { SolutionPageName } from '../../common/links';
@ -20,37 +19,3 @@ export const getSolutionPageNameFromNavLinkId = (navLinkId: string): SolutionPag
const fullId = cleanId.replace(`${APP_UI_ID}:`, ''); // remove Security appId if present
return fullId as SolutionPageName;
};
// We need to hide breadcrumbs for some pages (tabs) because they appear duplicated.
// These breadcrumbs are incorrectly processed as trailing breadcrumbs in SecuritySolution, because of `SpyRoute` architecture limitations.
// They are navLinks tree with a SecurityPageName, so they should be treated as leading breadcrumbs in ESS as well.
// TODO: Improve the breadcrumbs logic in `use_breadcrumbs_nav` to avoid this workaround.
const HIDDEN_BREADCRUMBS = new Set<SolutionPageName>([
SecurityPageName.networkFlows,
SecurityPageName.networkDns,
SecurityPageName.networkHttp,
SecurityPageName.networkTls,
SecurityPageName.networkAnomalies,
SecurityPageName.networkEvents,
SecurityPageName.usersAll,
SecurityPageName.usersAuthentications,
SecurityPageName.usersAnomalies,
SecurityPageName.usersRisk,
SecurityPageName.usersEvents,
SecurityPageName.hostsAll,
SecurityPageName.hostsUncommonProcesses,
SecurityPageName.hostsAnomalies,
SecurityPageName.hostsEvents,
SecurityPageName.hostsRisk,
SecurityPageName.hostsSessions,
]);
export const isBreadcrumbHidden = (id: SolutionPageName): boolean =>
HIDDEN_BREADCRUMBS.has(id) ||
/* management sub-pages set their breadcrumbs themselves, the main Management breadcrumb is configured with our navigationTree definition */
(id.startsWith(ExternalPageName.management) && id !== ExternalPageName.management);
export const isSideNavStatusHidden = (ids: SolutionPageName[]): boolean => {
// Dashboard nav children should not be visible. The Dashboard nav item should only navigate to a landing page
return ids[0] === SecurityPageName.dashboards;
};

View file

@ -37,10 +37,6 @@ const startMock = (): PluginStart => ({
getUpselling: () => upselling,
setOnboardingSettings: onboardingService.setSettings.bind(onboardingService),
setIsSolutionNavigationEnabled: jest.fn(),
getSolutionNavigation: jest.fn(async () => ({
navigationTree$: of({ body: [], footer: [] }),
panelContentProvider: jest.fn(),
})),
});
export const securitySolutionMock = {

View file

@ -57,7 +57,6 @@ export class PluginContract {
updateNavLinks(isSolutionNavigationEnabled, core);
},
setOnboardingSettings: this.onboardingService.setSettings.bind(this.onboardingService),
getSolutionNavigation: () => lazySolutionNavigation(core),
};
}
@ -81,10 +80,3 @@ const lazyResolver = async () => {
);
return resolverPluginSetup();
};
const lazySolutionNavigation = async (core: CoreStart) => {
const { getSolutionNavigation } = await import(
/* webpackChunkName: "solution_navigation" */
'./app/solution_navigation'
);
return getSolutionNavigation(core);
};

View file

@ -93,7 +93,6 @@ import type { ExperimentalFeatures } from '../common/experimental_features';
import type { SetComponents, GetComponents$ } from './contract_components';
import type { ConfigSettings } from '../common/config_settings';
import type { OnboardingService } from './onboarding/service';
import type { SolutionNavigation } from './app/solution_navigation/solution_navigation';
import type { TelemetryServiceStart } from './common/lib/telemetry';
import type { SiemMigrationsService } from './siem_migrations/service';
@ -222,7 +221,6 @@ export interface PluginStart {
getUpselling: () => UpsellingService;
setOnboardingSettings: OnboardingService['setSettings'];
setIsSolutionNavigationEnabled: (isSolutionNavigationEnabled: boolean) => void;
getSolutionNavigation: () => Promise<SolutionNavigation>;
}
export type InspectResponse = Inspect & { response: string[] };

View file

@ -191,8 +191,6 @@
"@kbn/core-http-request-handler-context-server",
"@kbn/core-http-server-mocks",
"@kbn/data-service",
"@kbn/core-chrome-browser",
"@kbn/shared-ux-chrome-navigation",
"@kbn/core-ui-settings-browser-mocks",
"@kbn/management-plugin",
"@kbn/security-plugin-types-server",

View file

@ -0,0 +1,729 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ESS Security Side Nav registers the navigation tree definition 1`] = `
Object {
"body": Array [
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "discover:",
"link": "discover",
},
Object {
"children": Array [
Object {
"id": "overview",
"link": "securitySolutionUI:overview",
"sideNavStatus": "hidden",
},
Object {
"id": "detection_response",
"link": "securitySolutionUI:detection_response",
"sideNavStatus": "hidden",
},
Object {
"id": "cloud_security_posture-dashboard",
"link": "securitySolutionUI:cloud_security_posture-dashboard",
"sideNavStatus": "hidden",
},
Object {
"id": "cloud_security_posture-vulnerability_dashboard",
"link": "securitySolutionUI:cloud_security_posture-vulnerability_dashboard",
"sideNavStatus": "hidden",
},
Object {
"id": "entity_analytics",
"link": "securitySolutionUI:entity_analytics",
"sideNavStatus": "hidden",
},
Object {
"id": "data_quality",
"link": "securitySolutionUI:data_quality",
"sideNavStatus": "hidden",
},
],
"id": "dashboards",
"link": "securitySolutionUI:dashboards",
"renderAs": "item",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"children": Array [
Object {
"id": "rules-add",
"link": "securitySolutionUI:rules-add",
},
Object {
"id": "rules-create",
"link": "securitySolutionUI:rules-create",
},
],
"id": "rules",
"link": "securitySolutionUI:rules",
"renderAs": "item",
},
Object {
"id": "cloud_security_posture-benchmarks",
"link": "securitySolutionUI:cloud_security_posture-benchmarks",
},
Object {
"id": "exceptions",
"link": "securitySolutionUI:exceptions",
},
Object {
"id": "siem_migrations-rules",
"link": "securitySolutionUI:siem_migrations-rules",
"title": "SIEM rule migrations",
},
],
"id": "category-management",
"title": "Management",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "coverage-overview",
"link": "securitySolutionUI:coverage-overview",
},
],
"id": "category-discover",
"title": "Discover",
},
],
"id": "rules-landing",
"link": "securitySolutionUI:rules-landing",
"renderAs": "panelOpener",
"title": "Rules",
},
Object {
"id": "alerts",
"link": "securitySolutionUI:alerts",
},
Object {
"id": "attack_discovery",
"link": "securitySolutionUI:attack_discovery",
},
Object {
"id": "cloud_security_posture-findings",
"link": "securitySolutionUI:cloud_security_posture-findings",
},
Object {
"children": Array [
Object {
"id": "cases_create",
"link": "securitySolutionUI:cases_create",
"sideNavStatus": "hidden",
},
Object {
"id": "cases_configure",
"link": "securitySolutionUI:cases_configure",
"sideNavStatus": "hidden",
},
],
"id": "cases",
"link": "securitySolutionUI:cases",
"renderAs": "panelOpener",
},
],
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"id": "timelines-templates",
"link": "securitySolutionUI:timelines-templates",
"sideNavStatus": "hidden",
},
],
"id": "timelines",
"link": "securitySolutionUI:timelines",
"renderAs": "item",
},
Object {
"id": "notes",
"link": "securitySolutionUI:notes",
"renderAs": "item",
},
Object {
"id": "osquery:",
"link": "osquery",
"renderAs": "item",
},
],
"id": "investigations",
"link": "securitySolutionUI:investigations",
"renderAs": "panelOpener",
"title": "Investigations",
},
Object {
"id": "threat_intelligence",
"link": "securitySolutionUI:threat_intelligence",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"id": "hosts-all",
"link": "securitySolutionUI:hosts-all",
},
Object {
"breadcrumbStatus": "hidden",
"id": "hosts-uncommon_processes",
"link": "securitySolutionUI:hosts-uncommon_processes",
},
Object {
"breadcrumbStatus": "hidden",
"id": "hosts-anomalies",
"link": "securitySolutionUI:hosts-anomalies",
},
Object {
"breadcrumbStatus": "hidden",
"id": "hosts-events",
"link": "securitySolutionUI:hosts-events",
},
Object {
"breadcrumbStatus": "hidden",
"id": "hosts-risk",
"link": "securitySolutionUI:hosts-risk",
},
Object {
"breadcrumbStatus": "hidden",
"id": "hosts-sessions",
"link": "securitySolutionUI:hosts-sessions",
},
],
"id": "hosts",
"link": "securitySolutionUI:hosts",
"renderAs": "item",
},
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"id": "network-flows",
"link": "securitySolutionUI:network-flows",
},
Object {
"breadcrumbStatus": "hidden",
"id": "network-dns",
"link": "securitySolutionUI:network-dns",
},
Object {
"breadcrumbStatus": "hidden",
"id": "network-http",
"link": "securitySolutionUI:network-http",
},
Object {
"breadcrumbStatus": "hidden",
"id": "network-tls",
"link": "securitySolutionUI:network-tls",
},
Object {
"breadcrumbStatus": "hidden",
"id": "network-anomalies",
"link": "securitySolutionUI:network-anomalies",
},
Object {
"breadcrumbStatus": "hidden",
"id": "network-events",
"link": "securitySolutionUI:network-events",
},
],
"id": "network",
"link": "securitySolutionUI:network",
"renderAs": "item",
},
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"id": "users-all",
"link": "securitySolutionUI:users-all",
},
Object {
"breadcrumbStatus": "hidden",
"id": "users-authentications",
"link": "securitySolutionUI:users-authentications",
},
Object {
"breadcrumbStatus": "hidden",
"id": "users-anomalies",
"link": "securitySolutionUI:users-anomalies",
},
Object {
"breadcrumbStatus": "hidden",
"id": "users-risk",
"link": "securitySolutionUI:users-risk",
},
Object {
"breadcrumbStatus": "hidden",
"id": "users-events",
"link": "securitySolutionUI:users-events",
},
],
"id": "users",
"link": "securitySolutionUI:users",
"renderAs": "item",
},
],
"id": "explore",
"link": "securitySolutionUI:explore",
"renderAs": "panelOpener",
"title": "Explore",
},
],
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"id": "fleet:agents",
"link": "fleet:agents",
},
Object {
"id": "fleet:policies",
"link": "fleet:policies",
"title": "Policies",
},
Object {
"id": "fleet:enrollment_tokens",
"link": "fleet:enrollment_tokens",
},
Object {
"id": "fleet:uninstall_tokens",
"link": "fleet:uninstall_tokens",
},
Object {
"id": "fleet:data_streams",
"link": "fleet:data_streams",
},
Object {
"id": "fleet:settings",
"link": "fleet:settings",
},
],
"id": "fleet:",
"link": "fleet",
"title": "Fleet",
},
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"id": "endpoints",
"link": "securitySolutionUI:endpoints",
},
Object {
"id": "policy",
"link": "securitySolutionUI:policy",
},
Object {
"id": "trusted_apps",
"link": "securitySolutionUI:trusted_apps",
},
Object {
"id": "event_filters",
"link": "securitySolutionUI:event_filters",
},
Object {
"id": "host_isolation_exceptions",
"link": "securitySolutionUI:host_isolation_exceptions",
},
Object {
"id": "blocklist",
"link": "securitySolutionUI:blocklist",
},
Object {
"id": "response_actions_history",
"link": "securitySolutionUI:response_actions_history",
},
],
"id": "endpoints",
"link": "securitySolutionUI:endpoints",
"title": "Endpoints",
},
Object {
"id": "assets_custom",
"renderItem": [Function],
"title": "",
},
],
"id": "assets",
"link": "securitySolutionUI:assets",
"renderAs": "panelOpener",
"title": "Assets",
},
],
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"children": Array [
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "ml:overview",
"link": "ml:overview",
"title": "Overview",
},
Object {
"id": "ml:notifications",
"link": "ml:notifications",
"title": "Notifications",
},
Object {
"id": "ml:memoryUsage",
"link": "ml:memoryUsage",
"title": "Memory usage",
},
],
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "ml:anomalyDetection",
"link": "ml:anomalyDetection",
"title": "Jobs",
},
Object {
"id": "ml:anomalyExplorer",
"link": "ml:anomalyExplorer",
"title": "Anomaly explorer",
},
Object {
"id": "ml:singleMetricViewer",
"link": "ml:singleMetricViewer",
"title": "Single metric viewer",
},
Object {
"id": "ml:suppliedConfigurations",
"link": "ml:suppliedConfigurations",
"title": "Supplied configurations",
},
Object {
"id": "ml:settings",
"link": "ml:settings",
"title": "Settings",
},
],
"id": "category-anomaly_detection",
"title": "Anomaly detection",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "ml:dataFrameAnalytics",
"link": "ml:dataFrameAnalytics",
"title": "Jobs",
},
Object {
"id": "ml:resultExplorer",
"link": "ml:resultExplorer",
"title": "Result explorer",
},
Object {
"id": "ml:analyticsMap",
"link": "ml:analyticsMap",
"title": "Analytics map",
},
],
"id": "category-data_frame analytics",
"title": "Data frame analytics",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "ml:nodesOverview",
"link": "ml:nodesOverview",
"title": "Trained models",
},
],
"id": "category-model_management",
"title": "Model management",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "ml:fileUpload",
"link": "ml:fileUpload",
"title": "File data visualizer",
},
Object {
"id": "ml:indexDataVisualizer",
"link": "ml:indexDataVisualizer",
"title": "Data view data visualizer",
},
Object {
"id": "ml:esqlDataVisualizer",
"link": "ml:esqlDataVisualizer",
"title": "ES|QL data visualizer",
},
Object {
"id": "ml:dataDrift",
"link": "ml:dataDrift",
"title": "Data drift",
},
],
"id": "category-data_visualizer",
"title": "Data visualizer",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "ml:logRateAnalysis",
"link": "ml:logRateAnalysis",
"title": "Log rate analysis",
},
Object {
"id": "ml:logPatternAnalysis",
"link": "ml:logPatternAnalysis",
"title": "Log pattern analysis",
},
Object {
"id": "ml:changePointDetections",
"link": "ml:changePointDetections",
"title": "Change point detection",
},
],
"id": "category-aiops_labs",
"title": "Aiops labs",
},
],
"id": "machine_learning-landing",
"link": "securitySolutionUI:machine_learning-landing",
"renderAs": "panelOpener",
"title": "Machine learning",
},
],
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"id": "entity_analytics-management",
"link": "securitySolutionUI:entity_analytics-management",
"sideNavStatus": "hidden",
"title": "Entity Risk Score",
},
Object {
"id": "entity_analytics-entity_store_management",
"link": "securitySolutionUI:entity_analytics-entity_store_management",
"sideNavStatus": "hidden",
"title": "Entity Store",
},
],
},
],
"defaultIsCollapsed": false,
"icon": "logoSecurity",
"id": "security_solution_nav",
"isCollapsible": false,
"title": "Security",
"type": "navGroup",
},
],
"footer": Array [
Object {
"icon": "launch",
"link": "securitySolutionUI:get_started",
"type": "navItem",
},
Object {
"icon": "editorCodeBlock",
"link": "dev_tools",
"title": "Developer tools",
"type": "navItem",
},
Object {
"breadcrumbStatus": "hidden",
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"link": "management:ingest_pipelines",
},
Object {
"link": "management:pipelines",
},
],
"title": "Ingest",
},
Object {
"children": Array [
Object {
"link": "management:index_management",
},
Object {
"link": "management:index_lifecycle_management",
},
Object {
"link": "management:snapshot_restore",
},
Object {
"link": "management:rollup_jobs",
},
Object {
"link": "management:transform",
},
Object {
"link": "management:cross_cluster_replication",
},
Object {
"link": "management:remote_clusters",
},
Object {
"link": "management:migrate_data",
},
],
"title": "Data",
},
Object {
"children": Array [
Object {
"link": "management:triggersActions",
},
Object {
"link": "management:cases",
},
Object {
"link": "management:triggersActionsConnectors",
},
Object {
"link": "management:reporting",
},
Object {
"link": "management:jobsListLink",
},
Object {
"link": "management:watcher",
},
Object {
"link": "management:maintenanceWindows",
},
Object {
"link": "securitySolutionUI:entity_analytics-management",
},
Object {
"link": "securitySolutionUI:entity_analytics-entity_store_management",
},
],
"title": "Alerts and Insights",
},
Object {
"children": Array [
Object {
"link": "management:users",
},
Object {
"link": "management:roles",
},
Object {
"link": "management:api_keys",
},
Object {
"link": "management:role_mappings",
},
],
"title": "Security",
},
Object {
"children": Array [
Object {
"link": "management:dataViews",
},
Object {
"link": "management:filesManagement",
},
Object {
"link": "management:objects",
},
Object {
"link": "management:tags",
},
Object {
"link": "management:search_sessions",
},
Object {
"link": "management:aiAssistantManagementSelection",
},
Object {
"link": "management:spaces",
},
Object {
"link": "maps",
},
Object {
"link": "visualize",
},
Object {
"link": "graph",
},
Object {
"link": "canvas",
},
Object {
"link": "management:settings",
},
],
"title": "Kibana",
},
Object {
"children": Array [
Object {
"link": "management:license_management",
},
Object {
"link": "management:upgrade_assistant",
},
],
"title": "Stack",
},
],
"id": "stack_management",
"renderAs": "panelOpener",
"spaceBefore": null,
"title": "Stack Management",
},
Object {
"link": "monitoring",
},
Object {
"link": "integrations",
},
],
"icon": "gear",
"id": "category-management",
"title": "Management",
"type": "navGroup",
},
],
}
`;

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 { mockServices } from '../common/__mocks__/services.mock';
import { initSideNavigation } from './side_navigation';
describe('ESS Security Side Nav', () => {
const services = mockServices;
it('registers the navigation tree definition', (done) => {
const addSolutionNavigation = jest.spyOn(services.navigation, 'addSolutionNavigation');
expect(addSolutionNavigation).not.toHaveBeenCalled();
initSideNavigation(services);
expect(addSolutionNavigation).toHaveBeenCalled();
const [addSolutionNavigationArg] = addSolutionNavigation.mock.calls[0];
const { navigationTree$ } = addSolutionNavigationArg;
navigationTree$.subscribe({
next: (value) => {
expect(value).toMatchSnapshot();
},
error: (err) => done(err),
complete: () => done(),
});
});
});

View file

@ -1,120 +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 { map } from 'rxjs';
import produce from 'immer';
import { i18n } from '@kbn/i18n';
import { SecurityPageName, SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation';
import type { AppDeepLinkId, GroupDefinition, NodeDefinition } from '@kbn/core-chrome-browser';
import { type Services } from '../common/services';
export const SOLUTION_NAME = i18n.translate('xpack.securitySolutionEss.nav.solutionName', {
defaultMessage: 'Security',
});
export const initSideNavigation = async (services: Services) => {
const { securitySolution, navigation } = services;
navigation.isSolutionNavEnabled$.subscribe((isSolutionNavigationEnabled) => {
securitySolution.setIsSolutionNavigationEnabled(isSolutionNavigationEnabled);
});
const { navigationTree$, panelContentProvider } = await securitySolution.getSolutionNavigation();
const essNavigationTree$ = navigationTree$.pipe(
map((navigationTree) =>
produce(navigationTree, (draft) => {
const footerGroup: GroupDefinition | undefined = draft.footer?.find(
(node): node is GroupDefinition => node.type === 'navGroup'
);
const management = footerGroup?.children.find((child) => child.link === 'management');
if (management) {
management.renderAs = 'panelOpener';
management.id = 'stack_management';
management.spaceBefore = null;
management.children = stackManagementLinks;
delete management.link;
}
})
)
);
navigation.addSolutionNavigation({
id: 'security',
homePage: `${SECURITY_UI_APP_ID}:${SecurityPageName.landing}`,
title: SOLUTION_NAME,
icon: 'logoSecurity',
navigationTree$: essNavigationTree$,
panelContentProvider,
dataTestSubj: 'securitySolutionSideNav',
});
};
// Stack Management static node definition
const stackManagementLinks: Array<NodeDefinition<AppDeepLinkId, string, string>> = [
{
title: 'Ingest',
children: [{ link: 'management:ingest_pipelines' }, { link: 'management:pipelines' }],
},
{
title: 'Data',
children: [
{ link: 'management:index_management' },
{ link: 'management:index_lifecycle_management' },
{ link: 'management:snapshot_restore' },
{ link: 'management:rollup_jobs' },
{ link: 'management:transform' },
{ link: 'management:cross_cluster_replication' },
{ link: 'management:remote_clusters' },
{ link: 'management:migrate_data' },
],
},
{
title: 'Alerts and Insights',
children: [
{ link: 'management:triggersActions' },
{ link: 'management:cases' },
{ link: 'management:triggersActionsConnectors' },
{ link: 'management:reporting' },
{ link: 'management:jobsListLink' },
{ link: 'management:watcher' },
{ link: 'management:maintenanceWindows' },
{ link: `${SECURITY_UI_APP_ID}:${SecurityPageName.entityAnalyticsManagement}` },
{ link: `${SECURITY_UI_APP_ID}:${SecurityPageName.entityAnalyticsEntityStoreManagement}` },
],
},
{
title: 'Security',
children: [
{ link: 'management:users' },
{ link: 'management:roles' },
{ link: 'management:api_keys' },
{ link: 'management:role_mappings' },
],
},
{
title: 'Kibana',
children: [
{ link: 'management:dataViews' },
{ link: 'management:filesManagement' },
{ link: 'management:objects' },
{ link: 'management:tags' },
{ link: 'management:search_sessions' },
{ link: 'management:aiAssistantManagementSelection' },
{ link: 'management:spaces' },
{ link: 'maps' },
{ link: 'visualize' },
{ link: 'graph' },
{ link: 'canvas' },
{ link: 'management:settings' },
],
},
{
title: 'Stack',
children: [{ link: 'management:license_management' }, { link: 'management:upgrade_assistant' }],
},
];

View file

@ -0,0 +1,805 @@
/*
* 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 * as Rx from 'rxjs';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser';
import { i18n } from '@kbn/i18n';
import {
ExternalPageName,
NavigationProvider,
SECURITY_UI_APP_ID,
SecurityPageName,
} from '@kbn/security-solution-navigation';
import { LinkButton, securityLink, i18nStrings } from '@kbn/security-solution-navigation/links';
import { type Services } from '../common/services';
export const SOLUTION_NAME = i18n.translate('xpack.securitySolutionEss.nav.solutionName', {
defaultMessage: 'Security',
});
const createNavigationTree$ = (services: Services): Rx.Observable<NavigationTreeDefinition> => {
return Rx.of({
body: [
{
type: 'navGroup',
id: 'security_solution_nav',
title: SOLUTION_NAME,
icon: 'logoSecurity',
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
children: [
{
id: 'discover:',
link: 'discover',
},
{
id: 'dashboards',
link: securityLink(SecurityPageName.dashboards),
renderAs: 'item',
children: [
{
id: 'overview',
link: securityLink(SecurityPageName.overview),
sideNavStatus: 'hidden',
},
{
id: 'detection_response',
link: securityLink(SecurityPageName.detectionAndResponse),
sideNavStatus: 'hidden',
},
{
id: 'cloud_security_posture-dashboard',
link: securityLink(SecurityPageName.cloudSecurityPostureDashboard),
sideNavStatus: 'hidden',
},
{
id: 'cloud_security_posture-vulnerability_dashboard',
link: securityLink(SecurityPageName.cloudSecurityPostureVulnerabilityDashboard),
sideNavStatus: 'hidden',
},
{
id: 'entity_analytics',
link: securityLink(SecurityPageName.entityAnalytics),
sideNavStatus: 'hidden',
},
{
id: 'data_quality',
link: securityLink(SecurityPageName.dataQuality),
sideNavStatus: 'hidden',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'rules-landing',
link: securityLink(SecurityPageName.rulesLanding),
title: i18nStrings.rules.title,
children: [
{
id: 'category-management',
title: i18nStrings.rules.management.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'rules',
link: securityLink(SecurityPageName.rules),
renderAs: 'item',
children: [
{
id: 'rules-add',
link: securityLink(SecurityPageName.rulesAdd),
},
{
id: 'rules-create',
link: securityLink(SecurityPageName.rulesCreate),
},
],
},
{
id: 'cloud_security_posture-benchmarks',
link: securityLink(SecurityPageName.cloudSecurityPostureBenchmarks),
},
{
id: 'exceptions',
link: securityLink(SecurityPageName.exceptions),
},
{
id: 'siem_migrations-rules',
link: securityLink(SecurityPageName.siemMigrationsRules),
title: i18nStrings.rules.management.siemMigrationsRules,
},
],
},
{
id: 'category-discover',
title: i18nStrings.rules.management.discover,
breadcrumbStatus: 'hidden',
children: [
{
id: 'coverage-overview',
link: securityLink(SecurityPageName.coverageOverview),
},
],
},
],
renderAs: 'panelOpener',
},
{
id: 'alerts',
link: securityLink(SecurityPageName.alerts),
},
{
id: 'attack_discovery',
link: securityLink(SecurityPageName.attackDiscovery),
},
{
id: 'cloud_security_posture-findings',
link: securityLink(SecurityPageName.cloudSecurityPostureFindings),
},
{
id: 'cases',
link: securityLink(SecurityPageName.case),
children: [
{
id: 'cases_create',
link: securityLink(SecurityPageName.caseCreate),
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: securityLink(SecurityPageName.caseConfigure),
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'investigations',
link: securityLink(SecurityPageName.investigations),
title: i18nStrings.investigations.title,
children: [
{
id: 'timelines',
link: securityLink(SecurityPageName.timelines),
renderAs: 'item',
children: [
{
id: 'timelines-templates',
link: securityLink(SecurityPageName.timelinesTemplates),
sideNavStatus: 'hidden',
},
],
},
{
id: 'notes',
link: securityLink(SecurityPageName.notes),
renderAs: 'item',
},
{
id: 'osquery:',
link: 'osquery',
renderAs: 'item',
},
],
renderAs: 'panelOpener',
},
{
id: 'threat_intelligence',
link: securityLink(SecurityPageName.threatIntelligence),
},
{
id: 'explore',
link: securityLink(SecurityPageName.exploreLanding),
title: i18nStrings.explore.title,
children: [
{
id: 'hosts',
link: securityLink(SecurityPageName.hosts),
renderAs: 'item',
children: [
{
id: 'hosts-all',
link: securityLink(SecurityPageName.hostsAll),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-uncommon_processes',
link: securityLink(SecurityPageName.hostsUncommonProcesses),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-anomalies',
link: securityLink(SecurityPageName.hostsAnomalies),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-events',
link: securityLink(SecurityPageName.hostsEvents),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-risk',
link: securityLink(SecurityPageName.hostsRisk),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-sessions',
link: securityLink(SecurityPageName.hostsSessions),
breadcrumbStatus: 'hidden',
},
],
},
{
id: 'network',
link: securityLink(SecurityPageName.network),
renderAs: 'item',
children: [
{
id: 'network-flows',
link: securityLink(SecurityPageName.networkFlows),
breadcrumbStatus: 'hidden',
},
{
id: 'network-dns',
link: securityLink(SecurityPageName.networkDns),
breadcrumbStatus: 'hidden',
},
{
id: 'network-http',
link: securityLink(SecurityPageName.networkHttp),
breadcrumbStatus: 'hidden',
},
{
id: 'network-tls',
link: securityLink(SecurityPageName.networkTls),
breadcrumbStatus: 'hidden',
},
{
id: 'network-anomalies',
link: securityLink(SecurityPageName.networkAnomalies),
breadcrumbStatus: 'hidden',
},
{
id: 'network-events',
link: securityLink(SecurityPageName.networkEvents),
breadcrumbStatus: 'hidden',
},
],
},
{
id: 'users',
link: securityLink(SecurityPageName.users),
renderAs: 'item',
children: [
{
id: 'users-all',
link: securityLink(SecurityPageName.usersAll),
breadcrumbStatus: 'hidden',
},
{
id: 'users-authentications',
link: securityLink(SecurityPageName.usersAuthentications),
breadcrumbStatus: 'hidden',
},
{
id: 'users-anomalies',
link: securityLink(SecurityPageName.usersAnomalies),
breadcrumbStatus: 'hidden',
},
{
id: 'users-risk',
link: securityLink(SecurityPageName.usersRisk),
breadcrumbStatus: 'hidden',
},
{
id: 'users-events',
link: securityLink(SecurityPageName.usersEvents),
breadcrumbStatus: 'hidden',
},
],
},
],
renderAs: 'panelOpener',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'assets',
link: securityLink(SecurityPageName.assets),
title: i18nStrings.assets.title,
children: [
{
id: 'fleet:',
link: 'fleet',
title: i18nStrings.assets.fleet.title,
children: [
{
id: 'fleet:agents',
link: 'fleet:agents',
},
{
id: 'fleet:policies',
link: 'fleet:policies',
title: i18nStrings.assets.fleet.policies,
},
{
id: 'fleet:enrollment_tokens',
link: 'fleet:enrollment_tokens',
},
{
id: 'fleet:uninstall_tokens',
link: 'fleet:uninstall_tokens',
},
{
id: 'fleet:data_streams',
link: 'fleet:data_streams',
},
{
id: 'fleet:settings',
link: 'fleet:settings',
},
],
},
{
id: 'endpoints',
link: securityLink(SecurityPageName.endpoints),
title: i18nStrings.assets.endpoints.title,
children: [
{
id: 'endpoints',
link: securityLink(SecurityPageName.endpoints),
breadcrumbStatus: 'hidden',
},
{
id: 'policy',
link: securityLink(SecurityPageName.policies),
},
{
id: 'trusted_apps',
link: securityLink(SecurityPageName.trustedApps),
},
{
id: 'event_filters',
link: securityLink(SecurityPageName.eventFilters),
},
{
id: 'host_isolation_exceptions',
link: securityLink(SecurityPageName.hostIsolationExceptions),
},
{
id: 'blocklist',
link: securityLink(SecurityPageName.blocklist),
},
{
id: 'response_actions_history',
link: securityLink(SecurityPageName.responseActionsHistory),
},
],
},
{
id: 'assets_custom',
title: '',
renderItem: () => {
return (
<>
<EuiSpacer />
<EuiCallOut
iconType="cluster"
title={i18n.translate(
'xpack.securitySolutionEss.nav.assets.integrationsCallout.title',
{ defaultMessage: 'Integrations' }
)}
>
<p>
{i18n.translate(
'xpack.securitySolutionEss.nav.assets.integrationsCallout.body',
{
defaultMessage:
'Choose an integration to start collecting and analyzing your data.',
}
)}
</p>
<EuiFlexGroup>
<EuiFlexItem>
<NavigationProvider core={services}>
<LinkButton id={ExternalPageName.integrationsSecurity} fill>
{i18n.translate(
'xpack.securitySolutionEss.nav.assets.integrationsCallout.button',
{ defaultMessage: 'Browse integrations' }
)}
</LinkButton>
</NavigationProvider>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
</>
);
},
},
],
renderAs: 'panelOpener',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'machine_learning-landing',
link: securityLink(SecurityPageName.mlLanding),
title: i18nStrings.ml.title,
children: [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:overview',
link: 'ml:overview',
title: i18nStrings.ml.overview,
},
{
id: 'ml:notifications',
link: 'ml:notifications',
title: i18nStrings.ml.notifications,
},
{
id: 'ml:memoryUsage',
link: 'ml:memoryUsage',
title: i18nStrings.ml.memoryUsage,
},
],
},
{
id: 'category-anomaly_detection',
title: i18nStrings.ml.anomalyDetection.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:anomalyDetection',
link: 'ml:anomalyDetection',
title: i18nStrings.ml.anomalyDetection.jobs,
},
{
id: 'ml:anomalyExplorer',
link: 'ml:anomalyExplorer',
title: i18nStrings.ml.anomalyDetection.anomalyExplorer,
},
{
id: 'ml:singleMetricViewer',
link: 'ml:singleMetricViewer',
title: i18nStrings.ml.anomalyDetection.singleMetricViewer,
},
{
id: 'ml:suppliedConfigurations',
link: 'ml:suppliedConfigurations',
title: i18nStrings.ml.anomalyDetection.suppliedConfigurations,
},
{
id: 'ml:settings',
link: 'ml:settings',
title: i18nStrings.ml.anomalyDetection.settings,
},
],
},
{
id: 'category-data_frame analytics',
title: i18nStrings.ml.dataFrameAnalytics.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:dataFrameAnalytics',
link: 'ml:dataFrameAnalytics',
title: i18nStrings.ml.dataFrameAnalytics.jobs,
},
{
id: 'ml:resultExplorer',
link: 'ml:resultExplorer',
title: i18nStrings.ml.dataFrameAnalytics.resultExplorer,
},
{
id: 'ml:analyticsMap',
link: 'ml:analyticsMap',
title: i18nStrings.ml.dataFrameAnalytics.analyticsMap,
},
],
},
{
id: 'category-model_management',
title: i18nStrings.ml.modelManagement.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:nodesOverview',
link: 'ml:nodesOverview',
title: i18nStrings.ml.modelManagement.trainedModels,
},
],
},
{
id: 'category-data_visualizer',
title: i18nStrings.ml.dataVisualizer.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:fileUpload',
link: 'ml:fileUpload',
title: i18nStrings.ml.dataVisualizer.fileDataVisualizer,
},
{
id: 'ml:indexDataVisualizer',
link: 'ml:indexDataVisualizer',
title: i18nStrings.ml.dataVisualizer.dataViewDataVisualizer,
},
{
id: 'ml:esqlDataVisualizer',
link: 'ml:esqlDataVisualizer',
title: i18nStrings.ml.dataVisualizer.esqlDataVisualizer,
},
{
id: 'ml:dataDrift',
link: 'ml:dataDrift',
title: i18nStrings.ml.dataVisualizer.dataDrift,
},
],
},
{
id: 'category-aiops_labs',
title: i18nStrings.ml.aiopsLabs.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:logRateAnalysis',
link: 'ml:logRateAnalysis',
title: i18nStrings.ml.aiopsLabs.logRateAnalysis,
},
{
id: 'ml:logPatternAnalysis',
link: 'ml:logPatternAnalysis',
title: i18nStrings.ml.aiopsLabs.logPatternAnalysis,
},
{
id: 'ml:changePointDetections',
link: 'ml:changePointDetections',
title: i18nStrings.ml.aiopsLabs.changePointDetection,
},
],
},
],
renderAs: 'panelOpener',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'entity_analytics-management',
link: securityLink(SecurityPageName.entityAnalyticsManagement),
title: i18nStrings.entityRiskScore,
sideNavStatus: 'hidden',
},
{
id: 'entity_analytics-entity_store_management',
link: securityLink(SecurityPageName.entityAnalyticsEntityStoreManagement),
title: i18nStrings.entityStore,
sideNavStatus: 'hidden',
},
],
},
],
isCollapsible: false,
},
],
footer: [
{
type: 'navItem',
link: securityLink(SecurityPageName.landing),
icon: 'launch',
},
{
type: 'navItem',
link: 'dev_tools',
title: i18nStrings.devTools,
icon: 'editorCodeBlock',
},
{
type: 'navGroup',
id: 'category-management',
title: i18nStrings.management.title,
icon: 'gear',
breadcrumbStatus: 'hidden',
children: [
{
title: i18nStrings.stackManagement.title,
renderAs: 'panelOpener',
id: 'stack_management',
spaceBefore: null,
children: [
{
title: i18nStrings.stackManagement.ingest.title,
children: [
{
link: 'management:ingest_pipelines',
},
{
link: 'management:pipelines',
},
],
},
{
title: i18nStrings.stackManagement.data.title,
children: [
{
link: 'management:index_management',
},
{
link: 'management:index_lifecycle_management',
},
{
link: 'management:snapshot_restore',
},
{
link: 'management:rollup_jobs',
},
{
link: 'management:transform',
},
{
link: 'management:cross_cluster_replication',
},
{
link: 'management:remote_clusters',
},
{
link: 'management:migrate_data',
},
],
},
{
title: i18nStrings.stackManagement.alertsAndInsights.title,
children: [
{
link: 'management:triggersActions',
},
{
link: 'management:cases',
},
{
link: 'management:triggersActionsConnectors',
},
{
link: 'management:reporting',
},
{
link: 'management:jobsListLink',
},
{
link: 'management:watcher',
},
{
link: 'management:maintenanceWindows',
},
{
link: securityLink(SecurityPageName.entityAnalyticsManagement),
},
{
link: securityLink(SecurityPageName.entityAnalyticsEntityStoreManagement),
},
],
},
{
title: i18nStrings.stackManagement.security.title,
children: [
{
link: 'management:users',
},
{
link: 'management:roles',
},
{
link: 'management:api_keys',
},
{
link: 'management:role_mappings',
},
],
},
{
title: i18nStrings.stackManagement.kibana.title,
children: [
{
link: 'management:dataViews',
},
{
link: 'management:filesManagement',
},
{
link: 'management:objects',
},
{
link: 'management:tags',
},
{
link: 'management:search_sessions',
},
{
link: 'management:aiAssistantManagementSelection',
},
{
link: 'management:spaces',
},
{
link: 'maps',
},
{
link: 'visualize',
},
{
link: 'graph',
},
{
link: 'canvas',
},
{
link: 'management:settings',
},
],
},
{
title: i18nStrings.stackManagement.stack.title,
children: [
{
link: 'management:license_management',
},
{
link: 'management:upgrade_assistant',
},
],
},
],
},
{
link: 'monitoring',
},
{
link: 'integrations',
},
],
},
],
});
};
export const initSideNavigation = async (services: Services) => {
const { securitySolution, navigation } = services;
navigation.isSolutionNavEnabled$.subscribe((isSolutionNavigationEnabled) => {
securitySolution.setIsSolutionNavigationEnabled(isSolutionNavigationEnabled);
});
navigation.addSolutionNavigation({
id: 'security',
homePage: `${SECURITY_UI_APP_ID}:${SecurityPageName.landing}`,
title: SOLUTION_NAME,
icon: 'logoSecurity',
navigationTree$: createNavigationTree$(services),
dataTestSubj: 'securitySolutionSideNav',
});
};

View file

@ -1,141 +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 { applyAiSocNavigation, aiGroup } from './ai_soc_navigation';
import { alertSummaryLink, configurationsLink } from './links';
import { ProductLine, ProductTier } from '../../../common/product';
import * as utils from './utils'; // We'll spy on the named export from here
import type { WritableDraft } from 'immer/dist/internal';
import type {
AppDeepLinkId,
GroupDefinition,
NavigationTreeDefinition,
NodeDefinition,
} from '@kbn/core-chrome-browser';
const nonAiProduct = { product_line: ProductLine.security, product_tier: ProductTier.essentials };
const aiProduct = { product_line: ProductLine.aiSoc, product_tier: ProductTier.essentials };
const getSampleDraft = (): WritableDraft<NavigationTreeDefinition<AppDeepLinkId>> => ({
body: [
{
type: 'navGroup',
id: 'security_solution_nav',
title: 'Security',
icon: 'logoSecurity',
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
children: [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'discover:',
link: 'discover',
title: 'Discover',
},
{
id: 'dashboards',
link: 'securitySolutionUI:dashboards',
title: 'Dashboards',
children: [
{
id: 'overview',
link: 'securitySolutionUI:overview',
title: 'Overview',
},
{
id: 'detection_response',
link: 'securitySolutionUI:detection_response',
title: 'Detection & Response',
},
{
id: 'entity_analytics',
link: 'securitySolutionUI:entity_analytics',
title: 'Entity Analytics',
},
{
id: 'data_quality',
link: 'securitySolutionUI:data_quality',
title: 'Data Quality',
},
],
renderAs: 'panelOpener',
},
],
},
],
isCollapsible: false,
},
],
});
describe('applyAiSocNavigation', () => {
let draft: WritableDraft<NavigationTreeDefinition<AppDeepLinkId>>;
beforeEach(() => {
draft = getSampleDraft();
});
describe('when productTypes does NOT include aiSoc', () => {
it('should not modify the navigation tree', () => {
const productTypes = [nonAiProduct];
const originalDraft = JSON.parse(JSON.stringify(draft));
applyAiSocNavigation(draft, productTypes);
// Should remain unchanged
expect(draft).toEqual(originalDraft);
});
});
describe('when productTypes includes aiSoc', () => {
let filterSpy: jest.SpyInstance;
beforeEach(() => {
// Spy on filterFromWhitelist so we can control the filter result
filterSpy = jest
.spyOn(utils, 'filterFromWhitelist')
.mockImplementation((nodes: Array<NodeDefinition<AppDeepLinkId>>, _whitelist: string[]) => {
// Simulate that the filter keeps only alertSummaryLink
return [alertSummaryLink];
});
});
afterEach(() => {
filterSpy.mockRestore();
});
it('should modify the navigation tree correctly', () => {
const productTypes = [aiProduct];
applyAiSocNavigation(draft, productTypes);
// The final draft.body should be replaced by one navGroup from aiGroup
// that has only the filtered children (we forced it to return [alertSummaryLink]).
expect(draft.body).toEqual([
{
...aiGroup,
children: [alertSummaryLink],
},
]);
// Check that filterFromWhitelist was called with the original children plus alertSummaryLink
const securityGroup = getSampleDraft().body[0] as WritableDraft<
GroupDefinition<AppDeepLinkId, string, string>
>;
const originalChildren = securityGroup.children;
const expectedChildrenForFiltering = [
...originalChildren,
alertSummaryLink,
configurationsLink,
];
expect(filterSpy).toHaveBeenCalledWith(expectedChildrenForFiltering, expect.any(Array));
});
});
});

View file

@ -5,71 +5,242 @@
* 2.0.
*/
import type {
AppDeepLinkId,
GroupDefinition,
NavigationTreeDefinition,
} from '@kbn/core-chrome-browser';
import * as Rx from 'rxjs';
import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser';
import { i18n } from '@kbn/i18n';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { i18nStrings, securityLink } from '@kbn/security-solution-navigation/links';
import type { WritableDraft } from 'immer/dist/internal';
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
import { alertSummaryLink, configurationsLink } from './links';
import { AiForTheSocIcon } from './icons';
import { filterFromWhitelist } from './utils';
import { type SecurityProductTypes } from '../../../common/config';
import { ProductLine } from '../../../common/product';
import { AiForTheSocIcon } from './icons';
import { createStackManagementNavigationTree } from '../stack_management_navigation';
const shouldUseAINavigation = (productTypes: SecurityProductTypes) => {
return productTypes.some((productType) => productType.product_line === ProductLine.aiSoc);
};
export const aiGroup: GroupDefinition<AppDeepLinkId, string, string> = {
type: 'navGroup',
id: 'security_solution_ai_nav',
title: 'AI for SOC',
icon: AiForTheSocIcon,
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
isCollapsible: false,
children: [],
};
// Elements we want to show in AI for SOC navigation
// This is a temporary solution until we figure out a way to handle Upselling with new Tier
const whitelist = [
SecurityPageName.case,
SecurityPageName.caseCreate,
SecurityPageName.caseConfigure,
SecurityPageName.alertSummary,
SecurityPageName.attackDiscovery,
SecurityPageName.configurations,
ExternalPageName.discover,
SecurityPageName.mlLanding,
];
// Apply AI for SOC navigation tree changes.
// The navigation tree received by parameter is generated at: x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/navigation_tree.ts
// An example of static navigation tree: x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts
// !! This is a temporary solution until the "classic" navigation is deprecated and the "generated" navigationTree is replaced by a static navigationTree (probably multiple of them).
export const applyAiSocNavigation = (
draft: WritableDraft<NavigationTreeDefinition<AppDeepLinkId>>,
productTypes: SecurityProductTypes
): void => {
if (!shouldUseAINavigation(productTypes)) {
return;
}
const securityGroup = draft.body[0] as WritableDraft<
GroupDefinition<AppDeepLinkId, string, string>
>;
// hardcode elements existing only in AI for SOC group
securityGroup.children.push(alertSummaryLink, configurationsLink);
// Overwrite the children with only the elements available for AI for SOC navigation
// Temporary solution until we have clarity how to proceed with Upselling in the new Tier
// (eg. Threat Intelligence couldn't be hidden)
securityGroup.children = filterFromWhitelist(securityGroup.children, whitelist);
draft.body = [{ ...aiGroup, children: securityGroup.children }];
const SOLUTION_NAME = i18n.translate(
'xpack.securitySolutionServerless.socNavLinks.projectType.title',
{ defaultMessage: 'AI for SOC' }
);
export const shouldUseAINavigation = (productTypes: SecurityProductTypes) =>
productTypes.some((productType) => productType.product_line === ProductLine.aiSoc);
export const createAiSocNavigationTree$ = (): Rx.Observable<NavigationTreeDefinition> => {
return Rx.of({
body: [
{
type: 'navGroup',
id: 'security_solution_ai_nav',
title: SOLUTION_NAME,
icon: AiForTheSocIcon,
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
isCollapsible: false,
children: [
{
id: 'discover:',
link: 'discover',
},
{
id: 'attack_discovery',
link: securityLink(SecurityPageName.attackDiscovery),
},
{
id: 'cases',
link: securityLink(SecurityPageName.case),
children: [
{
id: 'cases_create',
link: securityLink(SecurityPageName.caseCreate),
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: securityLink(SecurityPageName.caseConfigure),
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
{
id: 'machine_learning-landing',
link: securityLink(SecurityPageName.mlLanding),
children: [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:overview',
link: 'ml:overview',
title: i18nStrings.ml.overview,
},
{
id: 'ml:notifications',
link: 'ml:notifications',
title: i18nStrings.ml.notifications,
},
{
id: 'ml:memoryUsage',
link: 'ml:memoryUsage',
title: i18nStrings.ml.memoryUsage,
},
],
},
{
id: 'category-anomaly_detection',
title: i18nStrings.ml.anomalyDetection.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:anomalyDetection',
link: 'ml:anomalyDetection',
title: i18nStrings.ml.anomalyDetection.jobs,
},
{
id: 'ml:anomalyExplorer',
link: 'ml:anomalyExplorer',
title: i18nStrings.ml.anomalyDetection.anomalyExplorer,
},
{
id: 'ml:singleMetricViewer',
link: 'ml:singleMetricViewer',
title: i18nStrings.ml.anomalyDetection.singleMetricViewer,
},
{
id: 'ml:suppliedConfigurations',
link: 'ml:suppliedConfigurations',
title: i18nStrings.ml.anomalyDetection.suppliedConfigurations,
},
{
id: 'ml:settings',
link: 'ml:settings',
title: i18nStrings.ml.anomalyDetection.settings,
},
],
},
{
id: 'category-data_frame analytics',
title: i18nStrings.ml.dataFrameAnalytics.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:dataFrameAnalytics',
link: 'ml:dataFrameAnalytics',
title: i18nStrings.ml.dataFrameAnalytics.jobs,
},
{
id: 'ml:resultExplorer',
link: 'ml:resultExplorer',
title: i18nStrings.ml.dataFrameAnalytics.resultExplorer,
},
{
id: 'ml:analyticsMap',
link: 'ml:analyticsMap',
title: i18nStrings.ml.dataFrameAnalytics.analyticsMap,
},
],
},
{
id: 'category-model_management',
title: i18nStrings.ml.modelManagement.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:nodesOverview',
link: 'ml:nodesOverview',
title: i18nStrings.ml.modelManagement.trainedModels,
},
],
},
{
id: 'category-data_visualizer',
title: i18nStrings.ml.dataVisualizer.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:fileUpload',
link: 'ml:fileUpload',
title: i18nStrings.ml.dataVisualizer.fileDataVisualizer,
},
{
id: 'ml:indexDataVisualizer',
link: 'ml:indexDataVisualizer',
title: i18nStrings.ml.dataVisualizer.dataViewDataVisualizer,
},
{
id: 'ml:esqlDataVisualizer',
link: 'ml:esqlDataVisualizer',
title: i18nStrings.ml.dataVisualizer.esqlDataVisualizer,
},
{
id: 'ml:dataDrift',
link: 'ml:dataDrift',
title: i18nStrings.ml.dataVisualizer.dataDrift,
},
],
},
{
id: 'category-aiops_labs',
title: i18nStrings.ml.aiopsLabs.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:logRateAnalysis',
link: 'ml:logRateAnalysis',
title: i18nStrings.ml.aiopsLabs.logRateAnalysis,
},
{
id: 'ml:logPatternAnalysis',
link: 'ml:logPatternAnalysis',
title: i18nStrings.ml.aiopsLabs.logPatternAnalysis,
},
{
id: 'ml:changePointDetections',
link: 'ml:changePointDetections',
title: i18nStrings.ml.aiopsLabs.changePointDetection,
},
],
},
],
renderAs: 'panelOpener',
},
{
id: 'alert_summary',
link: securityLink(SecurityPageName.alertSummary),
},
{
id: 'configurations',
link: securityLink(SecurityPageName.configurations),
renderAs: 'panelOpener',
children: [
{
link: securityLink(SecurityPageName.configurationsAiSettings),
},
{
link: securityLink(SecurityPageName.configurationsBasicRules),
},
{
link: securityLink(SecurityPageName.configurationsIntegrations),
},
],
},
],
},
],
footer: [
{
type: 'navItem',
link: securityLink(SecurityPageName.landing),
icon: 'launch',
},
{
type: 'navItem',
link: 'dev_tools',
title: i18nStrings.devTools,
icon: 'editorCodeBlock',
},
createStackManagementNavigationTree(),
],
});
};

View file

@ -1,235 +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 { filterFromWhitelist } from './utils';
import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
describe('AI SOC utils', () => {
describe('filterFromWhitelist', () => {
const nodes: Array<NodeDefinition<AppDeepLinkId>> = [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'attack_discovery',
link: 'securitySolutionUI:attack_discovery',
title: 'Attack discovery',
},
{
id: 'cases',
link: 'securitySolutionUI:cases',
title: 'Cases',
children: [
{
id: 'cases_create',
link: 'securitySolutionUI:cases_create',
title: 'Create',
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: 'securitySolutionUI:cases_configure',
title: 'Settings',
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
],
},
];
it('should filter nodes based on whitelist of IDs', () => {
const idsToAttach = ['cases', 'attack_discovery'];
const result = filterFromWhitelist(nodes, idsToAttach);
expect(result).toEqual([
{
id: 'attack_discovery',
link: 'securitySolutionUI:attack_discovery',
title: 'Attack discovery',
},
{
id: 'cases',
link: 'securitySolutionUI:cases',
title: 'Cases',
children: [
{
id: 'cases_create',
link: 'securitySolutionUI:cases_create',
title: 'Create',
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: 'securitySolutionUI:cases_configure',
title: 'Settings',
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
]);
});
it('should handle empty nodes array', () => {
const result = filterFromWhitelist([], ['cases']);
expect(result).toEqual([]);
});
it('should handle empty idsToAttach array', () => {
const result = filterFromWhitelist(nodes, []);
expect(result).toEqual([]);
});
});
describe('filterFromWhitelist - Larger Dataset', () => {
let bigNodes: Array<NodeDefinition<AppDeepLinkId>>;
beforeEach(() => {
bigNodes = [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'discover:',
link: 'discover',
title: 'Discover',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'attack_discovery',
link: 'securitySolutionUI:attack_discovery',
title: 'Attack discovery',
},
{
id: 'cases',
link: 'securitySolutionUI:cases',
title: 'Cases',
children: [
{
id: 'cases_create',
link: 'securitySolutionUI:cases_create',
title: 'Create',
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: 'securitySolutionUI:cases_configure',
title: 'Settings',
sideNavStatus: 'hidden',
},
],
renderAs: 'panelOpener',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'threat_intelligence',
link: 'securitySolutionUI:threat_intelligence',
title: 'Intelligence',
},
],
},
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'assets',
link: 'securitySolutionUI:assets',
title: 'Assets',
children: [
{
id: 'fleet:',
link: 'fleet',
title: 'Fleet',
children: [
{
id: 'fleet:agents',
link: 'fleet:agents',
title: 'Agents',
},
{
id: 'fleet:policies',
link: 'fleet:policies',
title: 'Policies',
},
{
id: 'fleet:enrollment_tokens',
link: 'fleet:enrollment_tokens',
title: 'Enrollment tokens',
},
{
id: 'fleet:uninstall_tokens',
link: 'fleet:uninstall_tokens',
title: 'Uninstall tokens',
},
{
id: 'fleet:data_streams',
link: 'fleet:data_streams',
title: 'Data streams',
},
{
id: 'fleet:settings',
link: 'fleet:settings',
title: 'Settings',
},
],
},
],
renderAs: 'panelOpener',
},
],
},
{
id: 'management:securityAiAssistantManagement',
link: 'management:securityAiAssistantManagement',
title: 'Knowledge sources',
},
];
});
it('should whitelist multiple nodes from deep in the structure', () => {
const result = filterFromWhitelist(bigNodes, ['fleet:policies', 'cases_create']);
expect(result).toHaveLength(2);
expect(result.map((n) => n.id).sort()).toEqual(['cases_create', 'fleet:policies'].sort());
});
it('should preserve entire whitelisted node with children if matched directly', () => {
const result = filterFromWhitelist(bigNodes, ['assets']);
expect(result).toHaveLength(1);
expect(result[0].id).toEqual('assets');
expect(result[0].children).toBeDefined();
expect(result[0].children![0].id).toEqual('fleet:');
});
it('should handle no matches found in nested structure', () => {
const result = filterFromWhitelist(bigNodes, ['random1', 'random2']);
expect(result).toEqual([]);
});
it('should handle partial matches plus top-level matches', () => {
const result = filterFromWhitelist(bigNodes, ['threat_intelligence', 'fleet:data_streams']);
expect(result).toHaveLength(2);
expect(result.map((r) => r.id).sort()).toEqual(
['fleet:data_streams', 'threat_intelligence'].sort()
);
});
it('should work if we match the top-level `discover:` node plus a nested fleet child', () => {
const result = filterFromWhitelist(bigNodes, ['discover:', 'fleet:agents']);
expect(result).toHaveLength(2);
expect(result.map((n) => n.id).sort()).toEqual(['discover:', 'fleet:agents'].sort());
});
});
});

View file

@ -1,29 +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 type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
// Filter nodes from a whitelist of IDs, handling nested structures
export const filterFromWhitelist = (
nodes: Array<NodeDefinition<AppDeepLinkId>>,
idsToAttach: string[]
): Array<NodeDefinition<AppDeepLinkId>> => {
const attachedNodes: Array<NodeDefinition<AppDeepLinkId>> = [];
const stack: Array<NodeDefinition<AppDeepLinkId>> = [...nodes];
while (stack.length > 0) {
const node = stack.pop();
if (node) {
if (idsToAttach.includes(node.id as string)) {
attachedNodes.unshift(node);
} else if (node.children) {
stack.unshift(...node.children);
}
}
}
return attachedNodes;
};

View file

@ -0,0 +1,592 @@
/*
* 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 * as Rx from 'rxjs';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser';
import { i18n } from '@kbn/i18n';
import {
ExternalPageName,
NavigationProvider,
SecurityPageName,
} from '@kbn/security-solution-navigation';
import { LinkButton, i18nStrings, securityLink } from '@kbn/security-solution-navigation/links';
import { type Services } from '../common/services';
import { createStackManagementNavigationTree } from './stack_management_navigation';
const SOLUTION_NAME = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectType.title',
{ defaultMessage: 'Security' }
);
export const createSecurityNavigationTree$ = (
services: Services
): Rx.Observable<NavigationTreeDefinition> => {
return Rx.of({
body: [
{
type: 'navGroup',
id: 'security_solution_nav',
title: SOLUTION_NAME,
icon: 'logoSecurity',
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
children: [
{
id: 'discover:',
link: 'discover',
},
{
id: 'dashboards',
link: securityLink(SecurityPageName.dashboards),
renderAs: 'item',
children: [
{
id: 'overview',
link: securityLink(SecurityPageName.overview),
sideNavStatus: 'hidden',
},
{
id: 'detection_response',
link: securityLink(SecurityPageName.detectionAndResponse),
sideNavStatus: 'hidden',
},
{
id: 'cloud_security_posture-dashboard',
link: securityLink(SecurityPageName.cloudSecurityPostureDashboard),
sideNavStatus: 'hidden',
},
{
id: 'cloud_security_posture-vulnerability_dashboard',
link: securityLink(SecurityPageName.cloudSecurityPostureVulnerabilityDashboard),
sideNavStatus: 'hidden',
},
{
id: 'entity_analytics',
link: securityLink(SecurityPageName.entityAnalytics),
sideNavStatus: 'hidden',
},
{
id: 'data_quality',
link: securityLink(SecurityPageName.dataQuality),
sideNavStatus: 'hidden',
},
],
},
{
id: 'rules-landing',
link: securityLink(SecurityPageName.rulesLanding),
title: i18nStrings.rules.title,
children: [
{
id: 'category-management',
title: i18nStrings.rules.management.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'rules',
link: securityLink(SecurityPageName.rules),
renderAs: 'item',
children: [
{
id: 'rules-add',
link: securityLink(SecurityPageName.rulesAdd),
},
{
id: 'rules-create',
link: securityLink(SecurityPageName.rulesCreate),
},
],
},
{
id: 'cloud_security_posture-benchmarks',
link: securityLink(SecurityPageName.cloudSecurityPostureBenchmarks),
},
{
id: 'exceptions',
link: securityLink(SecurityPageName.exceptions),
},
{
id: 'siem_migrations-rules',
link: securityLink(SecurityPageName.siemMigrationsRules),
title: i18nStrings.rules.management.siemMigrationsRules,
},
],
},
{
id: 'category-discover',
title: i18nStrings.rules.management.discover,
breadcrumbStatus: 'hidden',
children: [
{
id: 'coverage-overview',
link: securityLink(SecurityPageName.coverageOverview),
},
],
},
],
renderAs: 'panelOpener',
},
{
id: 'alerts',
link: securityLink(SecurityPageName.alerts),
},
{
id: 'attack_discovery',
link: securityLink(SecurityPageName.attackDiscovery),
},
{
id: 'cloud_security_posture-findings',
link: securityLink(SecurityPageName.cloudSecurityPostureFindings),
},
{
id: 'cases',
link: securityLink(SecurityPageName.case),
children: [
{
id: 'cases_create',
link: securityLink(SecurityPageName.caseCreate),
sideNavStatus: 'hidden',
},
{
id: 'cases_configure',
link: securityLink(SecurityPageName.caseConfigure),
sideNavStatus: 'hidden',
},
],
renderAs: 'item',
},
{
id: 'investigations',
link: securityLink(SecurityPageName.investigations),
title: i18nStrings.investigations.title,
children: [
{
id: 'timelines',
link: securityLink(SecurityPageName.timelines),
renderAs: 'item',
children: [
{
id: 'timelines-templates',
link: securityLink(SecurityPageName.timelinesTemplates),
sideNavStatus: 'hidden',
},
],
},
{
id: 'notes',
link: securityLink(SecurityPageName.notes),
renderAs: 'item',
},
{
id: 'osquery:',
link: 'osquery',
renderAs: 'item',
},
],
renderAs: 'panelOpener',
},
{
id: 'threat_intelligence',
link: securityLink(SecurityPageName.threatIntelligence),
},
{
id: 'explore',
link: securityLink(SecurityPageName.exploreLanding),
title: i18nStrings.explore.title,
spaceBefore: null,
children: [
{
id: 'hosts',
link: securityLink(SecurityPageName.hosts),
renderAs: 'item',
children: [
{
id: 'hosts-all',
link: securityLink(SecurityPageName.hostsAll),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-uncommon_processes',
link: securityLink(SecurityPageName.hostsUncommonProcesses),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-anomalies',
link: securityLink(SecurityPageName.hostsAnomalies),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-events',
link: securityLink(SecurityPageName.hostsEvents),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-risk',
link: securityLink(SecurityPageName.hostsRisk),
breadcrumbStatus: 'hidden',
},
{
id: 'hosts-sessions',
link: securityLink(SecurityPageName.hostsSessions),
breadcrumbStatus: 'hidden',
},
],
},
{
id: 'network',
link: securityLink(SecurityPageName.network),
renderAs: 'item',
children: [
{
id: 'network-flows',
link: securityLink(SecurityPageName.networkFlows),
breadcrumbStatus: 'hidden',
},
{
id: 'network-dns',
link: securityLink(SecurityPageName.networkDns),
breadcrumbStatus: 'hidden',
},
{
id: 'network-http',
link: securityLink(SecurityPageName.networkHttp),
breadcrumbStatus: 'hidden',
},
{
id: 'network-tls',
link: securityLink(SecurityPageName.networkTls),
breadcrumbStatus: 'hidden',
},
{
id: 'network-anomalies',
link: securityLink(SecurityPageName.networkAnomalies),
breadcrumbStatus: 'hidden',
},
{
id: 'network-events',
link: securityLink(SecurityPageName.networkEvents),
breadcrumbStatus: 'hidden',
},
],
},
{
id: 'users',
link: securityLink(SecurityPageName.users),
renderAs: 'item',
children: [
{
id: 'users-all',
link: securityLink(SecurityPageName.usersAll),
breadcrumbStatus: 'hidden',
},
{
id: 'users-authentications',
link: securityLink(SecurityPageName.usersAuthentications),
breadcrumbStatus: 'hidden',
},
{
id: 'users-anomalies',
link: securityLink(SecurityPageName.usersAnomalies),
breadcrumbStatus: 'hidden',
},
{
id: 'users-risk',
link: securityLink(SecurityPageName.usersRisk),
breadcrumbStatus: 'hidden',
},
{
id: 'users-events',
link: securityLink(SecurityPageName.usersEvents),
breadcrumbStatus: 'hidden',
},
],
},
],
renderAs: 'panelOpener',
},
{
id: 'assets',
link: securityLink(SecurityPageName.assets),
title: i18nStrings.assets.title,
children: [
{
id: 'fleet:',
link: 'fleet',
title: i18nStrings.assets.fleet.title,
children: [
{
id: 'fleet:agents',
link: 'fleet:agents',
},
{
id: 'fleet:policies',
link: 'fleet:policies',
title: i18nStrings.assets.fleet.policies,
},
{
id: 'fleet:enrollment_tokens',
link: 'fleet:enrollment_tokens',
},
{
id: 'fleet:uninstall_tokens',
link: 'fleet:uninstall_tokens',
},
{
id: 'fleet:data_streams',
link: 'fleet:data_streams',
},
{
id: 'fleet:settings',
link: 'fleet:settings',
},
],
},
{
id: 'endpoints',
link: securityLink(SecurityPageName.endpoints),
title: i18nStrings.assets.endpoints.title,
children: [
{
id: 'endpoints',
link: securityLink(SecurityPageName.endpoints),
breadcrumbStatus: 'hidden',
},
{
id: 'policy',
link: securityLink(SecurityPageName.policies),
},
{
id: 'trusted_apps',
link: securityLink(SecurityPageName.trustedApps),
},
{
id: 'event_filters',
link: securityLink(SecurityPageName.eventFilters),
},
{
id: 'host_isolation_exceptions',
link: securityLink(SecurityPageName.hostIsolationExceptions),
},
{
id: 'blocklist',
link: securityLink(SecurityPageName.blocklist),
},
{
id: 'response_actions_history',
link: securityLink(SecurityPageName.responseActionsHistory),
},
],
},
{
id: 'assets_custom',
title: '',
renderItem: () => {
return (
<>
<EuiSpacer />
<EuiCallOut
iconType="cluster"
title={i18nStrings.assets.integrationsCallout.title}
>
<p>{i18nStrings.assets.integrationsCallout.body}</p>
<EuiFlexGroup>
<EuiFlexItem>
<NavigationProvider core={services}>
<LinkButton id={ExternalPageName.integrationsSecurity} fill>
{i18nStrings.assets.integrationsCallout.button}
</LinkButton>
</NavigationProvider>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
</>
);
},
},
],
renderAs: 'panelOpener',
},
{
id: 'machine_learning-landing',
link: securityLink(SecurityPageName.mlLanding),
title: i18nStrings.ml.title,
children: [
{
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:overview',
link: 'ml:overview',
title: i18nStrings.ml.overview,
},
{
id: 'ml:notifications',
link: 'ml:notifications',
title: i18nStrings.ml.notifications,
},
{
id: 'ml:memoryUsage',
link: 'ml:memoryUsage',
title: i18nStrings.ml.memoryUsage,
},
],
},
{
id: 'category-anomaly_detection',
title: i18nStrings.ml.anomalyDetection.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:anomalyDetection',
link: 'ml:anomalyDetection',
title: i18nStrings.ml.anomalyDetection.jobs,
},
{
id: 'ml:anomalyExplorer',
link: 'ml:anomalyExplorer',
title: i18nStrings.ml.anomalyDetection.anomalyExplorer,
},
{
id: 'ml:singleMetricViewer',
link: 'ml:singleMetricViewer',
title: i18nStrings.ml.anomalyDetection.singleMetricViewer,
},
{
id: 'ml:suppliedConfigurations',
link: 'ml:suppliedConfigurations',
title: i18nStrings.ml.anomalyDetection.suppliedConfigurations,
},
{
id: 'ml:settings',
link: 'ml:settings',
title: i18nStrings.ml.anomalyDetection.settings,
},
],
},
{
id: 'category-data_frame analytics',
title: i18nStrings.ml.dataFrameAnalytics.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:dataFrameAnalytics',
link: 'ml:dataFrameAnalytics',
title: i18nStrings.ml.dataFrameAnalytics.jobs,
},
{
id: 'ml:resultExplorer',
link: 'ml:resultExplorer',
title: i18nStrings.ml.dataFrameAnalytics.resultExplorer,
},
{
id: 'ml:analyticsMap',
link: 'ml:analyticsMap',
title: i18nStrings.ml.dataFrameAnalytics.analyticsMap,
},
],
},
{
id: 'category-model_management',
title: i18nStrings.ml.modelManagement.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:nodesOverview',
link: 'ml:nodesOverview',
title: i18nStrings.ml.modelManagement.trainedModels,
},
],
},
{
id: 'category-data_visualizer',
title: i18nStrings.ml.dataVisualizer.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:fileUpload',
link: 'ml:fileUpload',
title: i18nStrings.ml.dataVisualizer.fileDataVisualizer,
},
{
id: 'ml:indexDataVisualizer',
link: 'ml:indexDataVisualizer',
title: i18nStrings.ml.dataVisualizer.dataViewDataVisualizer,
},
{
id: 'ml:esqlDataVisualizer',
link: 'ml:esqlDataVisualizer',
title: i18nStrings.ml.dataVisualizer.esqlDataVisualizer,
},
{
id: 'ml:dataDrift',
link: 'ml:dataDrift',
title: i18nStrings.ml.dataVisualizer.dataDrift,
},
],
},
{
id: 'category-aiops_labs',
title: i18nStrings.ml.aiopsLabs.title,
breadcrumbStatus: 'hidden',
children: [
{
id: 'ml:logRateAnalysis',
link: 'ml:logRateAnalysis',
title: i18nStrings.ml.aiopsLabs.logRateAnalysis,
},
{
id: 'ml:logPatternAnalysis',
link: 'ml:logPatternAnalysis',
title: i18nStrings.ml.aiopsLabs.logPatternAnalysis,
},
{
id: 'ml:changePointDetections',
link: 'ml:changePointDetections',
title: i18nStrings.ml.aiopsLabs.changePointDetection,
},
],
},
],
renderAs: 'panelOpener',
},
{
id: 'entity_analytics-management',
link: securityLink(SecurityPageName.entityAnalyticsManagement),
title: i18nStrings.entityRiskScore,
sideNavStatus: 'hidden',
},
{
id: 'entity_analytics-entity_store_management',
link: securityLink(SecurityPageName.entityAnalyticsEntityStoreManagement),
title: i18nStrings.entityStore,
sideNavStatus: 'hidden',
},
],
isCollapsible: false,
},
],
footer: [
{
type: 'navItem',
link: securityLink(SecurityPageName.landing),
icon: 'launch',
},
{
type: 'navItem',
link: 'dev_tools',
title: i18nStrings.devTools,
icon: 'editorCodeBlock',
},
createStackManagementNavigationTree(),
],
});
};

View file

@ -0,0 +1,60 @@
/*
* 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 { ProductLine, ProductTier } from '../../common/product';
import { mockServices } from '../common/services/__mocks__/services.mock';
import { initSideNavigation } from './side_navigation';
describe('Security Side Nav', () => {
const services = mockServices;
const initNavigationSpy = jest.spyOn(services.serverless, 'initNavigation');
afterEach(() => {
initNavigationSpy.mockReset();
});
it('registers the navigation tree definition for serverless security', (done) => {
expect(initNavigationSpy).not.toHaveBeenCalled();
initSideNavigation(services, []);
expect(initNavigationSpy).toHaveBeenCalled();
const [, navigationTree$] = initNavigationSpy.mock.calls[0];
navigationTree$.subscribe({
next: (value) => {
expect(value).toMatchSnapshot();
},
error: (err) => done(err),
complete: () => done(),
});
});
it('registers the navigation tree definition for serverless security with AI SOC', (done) => {
expect(initNavigationSpy).not.toHaveBeenCalled();
initSideNavigation(services, [
{
product_line: 'ai_soc' as ProductLine,
product_tier: 'search_ai_lake' as ProductTier,
},
]);
expect(initNavigationSpy).toHaveBeenCalled();
const [, navigationTree$] = initNavigationSpy.mock.calls[0];
navigationTree$.subscribe({
next: (value) => {
expect(value).toMatchSnapshot();
},
error: (err) => done(err),
complete: () => done(),
});
});
});

View file

@ -5,18 +5,10 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { AppDeepLinkId, GroupDefinition, NodeDefinition } from '@kbn/core-chrome-browser';
import produce from 'immer';
import { map } from 'rxjs';
import type { SecurityProductTypes } from '../../common/config';
import { type Services } from '../common/services';
import { applyAiSocNavigation } from './ai_soc/ai_soc_navigation';
const PROJECT_SETTINGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.title',
{ defaultMessage: 'Project Settings' }
);
import { createAiSocNavigationTree$, shouldUseAINavigation } from './ai_soc/ai_soc_navigation';
import { createSecurityNavigationTree$ } from './security_side_navigation';
export const initSideNavigation = async (
services: Services,
@ -24,116 +16,11 @@ export const initSideNavigation = async (
) => {
services.securitySolution.setIsSolutionNavigationEnabled(true);
const { navigationTree$, panelContentProvider } =
await services.securitySolution.getSolutionNavigation();
const navigationTree$ = shouldUseAINavigation(productTypes)
? createAiSocNavigationTree$()
: createSecurityNavigationTree$(services);
const serverlessNavigationTree$ = navigationTree$.pipe(
map((navigationTree) =>
produce(navigationTree, (draft) => {
// Adds serverless cloud links to the footer group ("Product settings" dropdown)
const footerGroup: GroupDefinition | undefined = draft.footer?.find(
({ type }) => type === 'navGroup'
) as GroupDefinition;
const management = footerGroup?.children.find((child) => child.link === 'management');
if (management) {
management.renderAs = 'panelOpener';
management.id = 'stack_management';
management.spaceBefore = null;
management.children = stackManagementLinks;
delete management.link;
}
if (footerGroup) {
footerGroup.title = PROJECT_SETTINGS_TITLE;
footerGroup.children.push({ cloudLink: 'billingAndSub', openInNewTab: true });
}
applyAiSocNavigation(draft, productTypes);
})
)
);
services.serverless.initNavigation('security', serverlessNavigationTree$, {
panelContentProvider,
services.serverless.initNavigation('security', navigationTree$, {
dataTestSubj: 'securitySolutionSideNav',
});
};
// Stack Management static node definition
const stackManagementLinks: Array<NodeDefinition<AppDeepLinkId, string, string>> = [
{
title: i18n.translate('xpack.securitySolutionServerless.navLinks.projectSettings.mngt.data', {
defaultMessage: 'Data',
}),
breadcrumbStatus: 'hidden',
children: [
{ link: 'management:index_management', breadcrumbStatus: 'hidden' },
{ link: 'management:transform', breadcrumbStatus: 'hidden' },
{ link: 'management:ingest_pipelines', breadcrumbStatus: 'hidden' },
{ link: 'management:dataViews', breadcrumbStatus: 'hidden' },
{ link: 'management:jobsListLink', breadcrumbStatus: 'hidden' },
{ link: 'management:pipelines', breadcrumbStatus: 'hidden' },
{ link: 'management:data_quality', breadcrumbStatus: 'hidden' },
{ link: 'management:data_usage', breadcrumbStatus: 'hidden' },
],
},
{
title: i18n.translate('xpack.securitySolutionServerless.navLinks.projectSettings.mngt.access', {
defaultMessage: 'Access',
}),
breadcrumbStatus: 'hidden',
children: [
{ link: 'management:api_keys', breadcrumbStatus: 'hidden' },
{ link: 'management:roles', breadcrumbStatus: 'hidden' },
{
cloudLink: 'userAndRoles',
title: i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.mngt.usersAndRoles',
{ defaultMessage: 'Manage organization members' }
),
},
],
},
{
title: i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.mngt.alertsAndInsights',
{ defaultMessage: 'Alerts and Insights' }
),
breadcrumbStatus: 'hidden',
children: [
{ link: 'management:triggersActions', breadcrumbStatus: 'hidden' },
{ link: 'management:triggersActionsConnectors', breadcrumbStatus: 'hidden' },
{ link: 'management:maintenanceWindows', breadcrumbStatus: 'hidden' },
{ link: 'securitySolutionUI:entity_analytics-management', breadcrumbStatus: 'hidden' },
{
link: 'securitySolutionUI:entity_analytics-entity_store_management',
breadcrumbStatus: 'hidden',
},
],
},
{
title: i18n.translate(
'xpack.securitySolutionServerless.navLinks.projectSettings.mngt.content',
{ defaultMessage: 'Content' }
),
breadcrumbStatus: 'hidden',
children: [
{ link: 'management:spaces', breadcrumbStatus: 'hidden' },
{ link: 'management:objects', breadcrumbStatus: 'hidden' },
{ link: 'management:filesManagement', breadcrumbStatus: 'hidden' },
{ link: 'management:reporting', breadcrumbStatus: 'hidden' },
{ link: 'management:tags', breadcrumbStatus: 'hidden' },
{ link: 'maps' },
{ link: 'visualize' },
],
},
{
title: i18n.translate('xpack.securitySolutionServerless.navLinks.projectSettings.mngt.other', {
defaultMessage: 'Other',
}),
breadcrumbStatus: 'hidden',
children: [
{ link: 'management:settings', breadcrumbStatus: 'hidden' },
{ link: 'management:securityAiAssistantManagement', breadcrumbStatus: 'hidden' },
],
},
];

View file

@ -0,0 +1,162 @@
/*
* 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 { GroupDefinition } from '@kbn/core-chrome-browser';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { i18nStrings, securityLink } from '@kbn/security-solution-navigation/links';
export const createStackManagementNavigationTree = (): GroupDefinition => ({
type: 'navGroup',
id: 'category-management',
title: i18nStrings.projectSettings.title,
icon: 'gear',
breadcrumbStatus: 'hidden',
children: [
{
id: 'stack_management',
title: i18nStrings.stackManagement.title,
renderAs: 'panelOpener',
spaceBefore: null,
children: [
{
title: i18nStrings.stackManagement.data.title,
breadcrumbStatus: 'hidden',
children: [
{
link: 'management:index_management',
breadcrumbStatus: 'hidden',
},
{
link: 'management:transform',
breadcrumbStatus: 'hidden',
},
{
link: 'management:ingest_pipelines',
breadcrumbStatus: 'hidden',
},
{
link: 'management:dataViews',
breadcrumbStatus: 'hidden',
},
{
link: 'management:jobsListLink',
breadcrumbStatus: 'hidden',
},
{
link: 'management:pipelines',
breadcrumbStatus: 'hidden',
},
{
link: 'management:data_quality',
breadcrumbStatus: 'hidden',
},
{
link: 'management:data_usage',
breadcrumbStatus: 'hidden',
},
],
},
{
title: i18nStrings.stackManagement.access.title,
breadcrumbStatus: 'hidden',
children: [
{
link: 'management:api_keys',
breadcrumbStatus: 'hidden',
},
{
link: 'management:roles',
breadcrumbStatus: 'hidden',
},
{
cloudLink: 'userAndRoles',
title: i18nStrings.stackManagement.access.usersAndRoles,
},
],
},
{
title: i18nStrings.stackManagement.alertsAndInsights.title,
breadcrumbStatus: 'hidden',
children: [
{
link: 'management:triggersActions',
breadcrumbStatus: 'hidden',
},
{
link: 'management:triggersActionsConnectors',
breadcrumbStatus: 'hidden',
},
{
link: 'management:maintenanceWindows',
breadcrumbStatus: 'hidden',
},
{
link: securityLink(SecurityPageName.entityAnalyticsManagement),
breadcrumbStatus: 'hidden',
},
{
link: securityLink(SecurityPageName.entityAnalyticsEntityStoreManagement),
breadcrumbStatus: 'hidden',
},
],
},
{
title: i18nStrings.stackManagement.content.title,
breadcrumbStatus: 'hidden',
children: [
{
link: 'management:spaces',
breadcrumbStatus: 'hidden',
},
{
link: 'management:objects',
breadcrumbStatus: 'hidden',
},
{
link: 'management:filesManagement',
breadcrumbStatus: 'hidden',
},
{
link: 'management:reporting',
breadcrumbStatus: 'hidden',
},
{
link: 'management:tags',
breadcrumbStatus: 'hidden',
},
{
link: 'maps',
},
{
link: 'visualize',
},
],
},
{
title: i18nStrings.stackManagement.other.title,
breadcrumbStatus: 'hidden',
children: [
{
link: 'management:settings',
breadcrumbStatus: 'hidden',
},
{
link: 'management:securityAiAssistantManagement',
breadcrumbStatus: 'hidden',
},
],
},
],
},
{
link: 'integrations',
},
{
cloudLink: 'billingAndSub',
},
],
});

View file

@ -51,7 +51,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
// check the Investigations subsection
await solutionNavigation.sidenav.openPanel('investigations'); // open Investigations panel
await testSubjects.click(`~solutionSideNavPanelLink-timelines`);
await testSubjects.click(`~panelNavItem-id-timelines`);
await solutionNavigation.sidenav.expectLinkActive({ navId: 'investigations' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Timelines' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({

View file

@ -66,7 +66,6 @@ import {
OSQUERY_URL,
MACHINE_LEARNING_LANDING_URL,
ASSETS_URL,
FLEET_URL,
HOSTS_URL,
} from '../../../urls/navigation';
import { RULES_MANAGEMENT_URL } from '../../../urls/rules_management';
@ -275,7 +274,7 @@ describe('Serverless side navigation links', { tags: '@serverless' }, () => {
cy.url().should('include', RULES_MANAGEMENT_URL);
});
it('navigates to the Rules page', () => {
it('navigates to the CSP Benchmarks page', () => {
navigateFromHeaderTo(ServerlessHeaders.CSP_BENCHMARKS, true);
cy.url().should('include', CSP_BENCHMARKS_URL);
});
@ -352,10 +351,6 @@ describe('Serverless side navigation links', { tags: '@serverless' }, () => {
navigateFromHeaderTo(ServerlessHeaders.ENDPOINTS, true);
cy.url().should('include', ENDPOINTS_URL);
});
it('navigates to the Fleet page', () => {
navigateFromHeaderTo(ServerlessHeaders.FLEET, true);
cy.url().should('include', FLEET_URL);
});
it('navigates to the Machine learning landing page', () => {
navigateFromHeaderTo(ServerlessHeaders.MACHINE_LEARNING, true);
cy.url().should('include', MACHINE_LEARNING_LANDING_URL);

View file

@ -47,45 +47,44 @@ export const THREAT_INTELLIGENCE =
export const CASES = '[data-test-subj*="nav-item-deepLinkId-securitySolutionUI:cases"]';
// nested panel links
export const OVERVIEW = '[data-test-subj="solutionSideNavPanelLink-overview"]';
export const OVERVIEW = '[data-test-subj~="panelNavItem-id-overview"]';
export const DETECTION_RESPONSE = '[data-test-subj="solutionSideNavPanelLink-detection_response"]';
export const DETECTION_RESPONSE = '[data-test-subj~="panelNavItem-id-detection_response"]';
export const ENTITY_ANALYTICS = '[data-test-subj="solutionSideNavPanelLink-entity_analytics"]';
export const ENTITY_ANALYTICS = '[data-test-subj~="panelNavItem-id-entity_analytics"]';
export const TIMELINES = '[data-test-subj="solutionSideNavPanelLink-timelines"]';
export const OSQUERY = '[data-test-subj="solutionSideNavPanelLink-osquery:"]';
export const TIMELINES = '[data-test-subj~="panelNavItem-id-timelines"]';
export const OSQUERY = '[data-test-subj~="panelNavItem-id-osquery:"]';
export const KUBERNETES = '[data-test-subj="solutionSideNavPanelLink-kubernetes"]';
export const KUBERNETES = '[data-test-subj~="panelNavItem-id-kubernetes"]';
export const CSP_DASHBOARD =
'[data-test-subj="solutionSideNavPanelLink-cloud_security_posture-dashboard"]';
export const CSP_DASHBOARD = '[data-test-subj~="panelNavItem-id-cloud_security_posture-dashboard"]';
export const HOSTS = '[data-test-subj="solutionSideNavPanelLink-hosts"]';
export const HOSTS = '[data-test-subj~="panelNavItem-id-hosts"]';
export const FLEET = '[data-test-subj="solutionSideNavPanelLink-fleet:"]';
export const ENDPOINTS = '[data-test-subj="solutionSideNavPanelLink-endpoints"]';
export const FLEET = '[data-test-subj~="panelNavItem-id-fleet:"]';
export const ENDPOINTS = '[data-test-subj~="panelNavItem-id-endpoints"]';
export const POLICIES = '[data-test-subj="solutionSideNavPanelLink-policy"]';
export const POLICIES = '[data-test-subj~="panelNavItem-id-policy"]';
export const TRUSTED_APPS = '[data-test-subj="solutionSideNavPanelLink-trusted_apps"]';
export const TRUSTED_APPS = '[data-test-subj~="panelNavItem-id-trusted_apps"]';
export const EVENT_FILTERS = '[data-test-subj="solutionSideNavPanelLink-event_filters"]';
export const EVENT_FILTERS = '[data-test-subj~="panelNavItem-id-event_filters"]';
export const BLOCKLIST = '[data-test-subj="solutionSideNavPanelLink-blocklist"]';
export const BLOCKLIST = '[data-test-subj~="panelNavItem-id-blocklist"]';
export const CSP_BENCHMARKS =
'[data-test-subj="solutionSideNavPanelLink-cloud_security_posture-benchmarks"]';
'[data-test-subj~="panelNavItem-id-cloud_security_posture-benchmarks"]';
export const RULES_COVERAGE = '[data-test-subj="solutionSideNavPanelLink-coverage-overview"]';
export const RULES_COVERAGE = '[data-test-subj~="panelNavItem-id-coverage-overview"]';
export const NETWORK = '[data-test-subj="solutionSideNavPanelLink-network"]';
export const NETWORK = '[data-test-subj~="panelNavItem-id-network"]';
export const USERS = '[data-test-subj="solutionSideNavPanelLink-users"]';
export const USERS = '[data-test-subj~="panelNavItem-id-users"]';
export const RULES = '[data-test-subj="solutionSideNavPanelLink-rules"]';
export const RULES = '[data-test-subj~="panelNavItem-id-rules"]';
export const EXCEPTIONS = '[data-test-subj="solutionSideNavPanelLink-exceptions"]';
export const EXCEPTIONS = '[data-test-subj~="panelNavItem-id-exceptions"]';
export const getBreadcrumb = (deepLinkId: string) => {
return `breadcrumb-deepLinkId-${deepLinkId}`;

View file

@ -12,10 +12,10 @@ export function MachineLearningNavigationProviderSecurity({ getService }: FtrPro
async function navigateToArea(id: string) {
await testSubjects.click('~panelOpener-deepLinkId-securitySolutionUI:machine_learning-landing');
await testSubjects.existOrFail(`~solutionSideNavPanelLink-ml:${id}`, {
await testSubjects.existOrFail(`~panelNavItem-id-ml:${id}`, {
timeout: 60 * 1000,
});
await testSubjects.click(`~solutionSideNavPanelLink-ml:${id}`);
await testSubjects.click(`~panelNavItem-id-ml:${id}`);
}
return {