mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[AI4DSOC] Add navigation (#214382)
This commit is contained in:
parent
e5e42a87ea
commit
7083930b87
24 changed files with 644 additions and 28 deletions
|
@ -1 +1,5 @@
|
|||
# Security Search AI Lake tier config
|
||||
|
||||
## Disable plugins
|
||||
xpack.osquery.enabled: false
|
||||
|
||||
|
|
|
@ -85,4 +85,5 @@ export enum SecurityPageName {
|
|||
entityAnalyticsEntityStoreManagement = 'entity_analytics-entity_store_management',
|
||||
coverageOverview = 'coverage-overview',
|
||||
notes = 'notes',
|
||||
alertSummary = 'alert_summary',
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export enum ProductFeatureSecurityKey {
|
||||
/** Enables Advanced Insights (Entity Risk, GenAI) */
|
||||
advancedInsights = 'advanced_insights',
|
||||
/** Enables Alerts Summary page for AI SOC */
|
||||
alertsSummary = 'alerts_summary',
|
||||
/**
|
||||
* Enables Investigation guide in Timeline
|
||||
*/
|
||||
|
|
|
@ -42,6 +42,18 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
|
|||
},
|
||||
},
|
||||
},
|
||||
[ProductFeatureSecurityKey.alertsSummary]: {
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['alerts_summary'],
|
||||
api: [`${APP_ID}-alert-summary`],
|
||||
},
|
||||
read: {
|
||||
ui: ['alerts_summary_read'],
|
||||
api: [`${APP_ID}-alert-summary`],
|
||||
},
|
||||
},
|
||||
},
|
||||
[ProductFeatureSecurityKey.investigationGuideInteractions]: {
|
||||
privileges: {
|
||||
all: {
|
||||
|
|
|
@ -96,6 +96,7 @@ export const DETECTION_RESPONSE_PATH = '/detection_response' as const;
|
|||
export const DETECTIONS_PATH = '/detections' as const;
|
||||
export const ALERTS_PATH = '/alerts' as const;
|
||||
export const ALERT_DETAILS_REDIRECT_PATH = `${ALERTS_PATH}/redirect` as const;
|
||||
export const ALERT_SUMMARY_PATH = `/alert_summary` as const;
|
||||
export const RULES_PATH = '/rules' as const;
|
||||
export const RULES_LANDING_PATH = `${RULES_PATH}/landing` as const;
|
||||
export const RULES_ADD_PATH = `${RULES_PATH}/add_rules` as const;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { links as attackDiscoveryLinks } from './attack_discovery/links';
|
|||
import { links as assetInventoryLinks } from './asset_inventory/links';
|
||||
import type { AppLinkItems } from './common/links/types';
|
||||
import { indicatorsLinks } from './threat_intelligence/links';
|
||||
import { links as alertsLinks } from './detections/links';
|
||||
import { alertsLink, alertSummaryLink } from './detections/links';
|
||||
import { links as rulesLinks } from './rules/links';
|
||||
import { links as timelinesLinks } from './timelines/links';
|
||||
import { links as casesLinks } from './cases/links';
|
||||
|
@ -26,7 +26,8 @@ export { solutionAppLinksSwitcher } from './app/solution_navigation/links/app_li
|
|||
|
||||
export const appLinks: AppLinkItems = Object.freeze([
|
||||
dashboardsLinks,
|
||||
alertsLinks,
|
||||
alertsLink,
|
||||
alertSummaryLink,
|
||||
attackDiscoveryLinks,
|
||||
findingsLinks,
|
||||
casesLinks,
|
||||
|
@ -47,7 +48,8 @@ export const getFilteredLinks = async (
|
|||
|
||||
return Object.freeze([
|
||||
dashboardsLinks,
|
||||
alertsLinks,
|
||||
alertsLink,
|
||||
alertSummaryLink,
|
||||
attackDiscoveryLinks,
|
||||
findingsLinks,
|
||||
casesLinks,
|
||||
|
|
|
@ -5,11 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERTS_PATH, SecurityPageName, SECURITY_FEATURE_ID } from '../../common/constants';
|
||||
import {
|
||||
ALERTS_PATH,
|
||||
SecurityPageName,
|
||||
SECURITY_FEATURE_ID,
|
||||
ALERT_SUMMARY_PATH,
|
||||
} from '../../common/constants';
|
||||
import { ALERTS } from '../app/translations';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
||||
export const links: LinkItem = {
|
||||
export const alertsLink: LinkItem = {
|
||||
id: SecurityPageName.alerts,
|
||||
title: ALERTS,
|
||||
path: ALERTS_PATH,
|
||||
|
@ -21,3 +26,17 @@ export const links: LinkItem = {
|
|||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const alertSummaryLink: LinkItem = {
|
||||
id: SecurityPageName.alertSummary,
|
||||
path: ALERT_SUMMARY_PATH,
|
||||
title: 'Alert summary',
|
||||
capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${SECURITY_FEATURE_ID}.alerts_summary`]],
|
||||
globalNavPosition: 3,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.alertSummary', {
|
||||
defaultMessage: 'Alert summary',
|
||||
}),
|
||||
],
|
||||
hideTimeline: true,
|
||||
};
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
import type { Capabilities } from '@kbn/core/public';
|
||||
import { SECURITY_FEATURE_ID } from '../common/constants';
|
||||
|
||||
export function hasAccessToSecuritySolution(capabilities: Capabilities) {
|
||||
return (
|
||||
export function hasAccessToSecuritySolution(capabilities: Capabilities): boolean {
|
||||
return Boolean(
|
||||
// Using `siemV2`
|
||||
capabilities[SECURITY_FEATURE_ID]?.show === true
|
||||
capabilities[SECURITY_FEATURE_ID]?.show ||
|
||||
capabilities.securitySolutionCasesV2?.read_cases ||
|
||||
capabilities.securitySolutionAttackDiscovery?.['attack-discovery']
|
||||
);
|
||||
}
|
||||
|
|
|
@ -435,16 +435,11 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
const { capabilities } = core.application;
|
||||
const { upsellingService, isSolutionNavigationEnabled$ } = this.contract;
|
||||
|
||||
// When the user does not have access to SIEM (main Security feature) nor Security Cases feature, the plugin must be inaccessible.
|
||||
if (
|
||||
!hasAccessToSecuritySolution(capabilities) &&
|
||||
!capabilities.securitySolutionCasesV2?.read_cases
|
||||
) {
|
||||
this.appUpdater$.next(() => ({
|
||||
status: AppStatus.inaccessible,
|
||||
visibleIn: [],
|
||||
}));
|
||||
// no need to register the links updater when the plugin is inaccessible
|
||||
// When the user does not have any of the capabilities required to access security solution, the plugin should be inaccessible
|
||||
// This is necessary to hide security solution from the selectable solutions in the spaces UI
|
||||
if (!hasAccessToSecuritySolution(capabilities)) {
|
||||
this.appUpdater$.next(() => ({ status: AppStatus.inaccessible, visibleIn: [] }));
|
||||
// no need to register the links updater when the plugin is inaccessible. return early
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -329,7 +329,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
licensing: plugins.licensing,
|
||||
scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({
|
||||
endpointAppContextService: this.endpointAppContextService,
|
||||
osqueryCreateActionService: plugins.osquery.createActionService,
|
||||
osqueryCreateActionService: plugins.osquery?.createActionService,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -5,8 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALL_PRODUCT_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
|
||||
import type { ProductFeatureKeyType } from '@kbn/security-solution-features/keys';
|
||||
import {
|
||||
ALL_PRODUCT_FEATURE_KEYS,
|
||||
ProductFeatureSecurityKey,
|
||||
} from '@kbn/security-solution-features/keys';
|
||||
|
||||
// Just copying all feature keys for now.
|
||||
// We may need a different set of keys in the future if we create serverless-specific productFeatures
|
||||
export const DEFAULT_PRODUCT_FEATURES = [...ALL_PRODUCT_FEATURE_KEYS];
|
||||
// List of product features that are disabled in different offering (eg. Serverless).
|
||||
const DISABLED_PRODUCT_FEATURES: ProductFeatureKeyType[] = [
|
||||
ProductFeatureSecurityKey.alertsSummary,
|
||||
];
|
||||
|
||||
export const DEFAULT_PRODUCT_FEATURES = ALL_PRODUCT_FEATURE_KEYS.filter(
|
||||
(key) => !DISABLED_PRODUCT_FEATURES.includes(key as ProductFeatureKeyType)
|
||||
);
|
||||
|
|
|
@ -16,7 +16,11 @@ type PliProductFeatures = Readonly<
|
|||
|
||||
export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
|
||||
[ProductLine.aiSoc]: {
|
||||
search_ai_lake: [ProductFeatureKey.attackDiscovery, ProductFeatureKey.assistant],
|
||||
search_ai_lake: [
|
||||
ProductFeatureKey.attackDiscovery,
|
||||
ProductFeatureKey.assistant,
|
||||
ProductFeatureKey.alertsSummary,
|
||||
],
|
||||
essentials: [ProductFeatureKey.attackDiscovery, ProductFeatureKey.assistant],
|
||||
complete: [ProductFeatureKey.attackDiscovery, ProductFeatureKey.assistant],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
export const AiForTheSoc: React.FC<SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill="#F04E98"
|
||||
fillRule="evenodd"
|
||||
d="M5.625 4.38V0h12.5v10.465c0 2.446-3.986 4.048-5.635 4.535V4.38H5.625Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill="#FACB3D"
|
||||
fillRule="evenodd"
|
||||
d="M1.875 12.546V6.25h8.75V20s-8.75-3.769-8.75-7.454Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill="#0B64DD"
|
||||
fillRule="evenodd"
|
||||
d="M5.625 6.25h5V15c-1.865-.713-5-2.348-5-4.402V6.25Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AiForTheSoc;
|
|
@ -47,3 +47,6 @@ export const IconRapidBarGraphLazy = withSuspenseIcon(
|
|||
export const IconFilebeatChartLazy = withSuspenseIcon(
|
||||
React.lazy(() => import('./icons/filebeat_chart'))
|
||||
);
|
||||
export const IconAiForTheSocLazy = withSuspenseIcon(
|
||||
React.lazy(() => import('./icons/ai_for_the_soc'))
|
||||
);
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 } 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];
|
||||
|
||||
expect(filterSpy).toHaveBeenCalledWith(expectedChildrenForFiltering, expect.any(Array));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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,
|
||||
GroupDefinition,
|
||||
NavigationTreeDefinition,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
|
||||
import type { WritableDraft } from 'immer/dist/internal';
|
||||
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import { alertSummaryLink } from './links';
|
||||
import { AiForTheSocIcon } from './icons';
|
||||
import { filterFromWhitelist } from './utils';
|
||||
import { type SecurityProductTypes } from '../../../common/config';
|
||||
import { ProductLine } from '../../../common/product';
|
||||
|
||||
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,
|
||||
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);
|
||||
|
||||
// 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 }];
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { EuiIconProps } from '@elastic/eui';
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { IconAiForTheSocLazy } from '../../common/lazy_icons';
|
||||
|
||||
export const AiForTheSocIcon = ({ size = 'm', ...rest }: Omit<EuiIconProps, 'type'>) => {
|
||||
return <EuiIcon {...{ type: IconAiForTheSocLazy, size, ...rest }} />;
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
|
||||
import { ALERT_SUMMARY } from './translations';
|
||||
|
||||
export const alertSummaryLink: NodeDefinition<AppDeepLinkId, string, string> = {
|
||||
id: SecurityPageName.alertSummary,
|
||||
link: 'securitySolutionUI:alert_summary',
|
||||
title: ALERT_SUMMARY,
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 ALERT_SUMMARY = i18n.translate(
|
||||
'xpack.securitySolutionServerless.navigation.aiSoc.alertSummary',
|
||||
{
|
||||
defaultMessage: 'Alert summary',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* 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());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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;
|
||||
};
|
|
@ -6,15 +6,16 @@
|
|||
*/
|
||||
|
||||
import { APP_PATH } from '@kbn/security-solution-plugin/common';
|
||||
import type { SecurityProductTypes } from '../../common/config';
|
||||
import type { Services } from '../common/services';
|
||||
import { subscribeBreadcrumbs } from './breadcrumbs';
|
||||
import { initSideNavigation } from './side_navigation';
|
||||
import { enableManagementCardsLanding } from './management_cards';
|
||||
|
||||
export const startNavigation = (services: Services) => {
|
||||
export const startNavigation = (services: Services, productTypes: SecurityProductTypes) => {
|
||||
services.serverless.setProjectHome(APP_PATH);
|
||||
|
||||
initSideNavigation(services);
|
||||
initSideNavigation(services, productTypes);
|
||||
enableManagementCardsLanding(services);
|
||||
subscribeBreadcrumbs(services);
|
||||
};
|
||||
|
|
|
@ -9,14 +9,19 @@ 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' }
|
||||
);
|
||||
|
||||
export const initSideNavigation = async (services: Services) => {
|
||||
export const initSideNavigation = async (
|
||||
services: Services,
|
||||
productTypes: SecurityProductTypes
|
||||
) => {
|
||||
services.securitySolution.setIsSolutionNavigationEnabled(true);
|
||||
|
||||
const { navigationTree$, panelContentProvider } =
|
||||
|
@ -41,6 +46,8 @@ export const initSideNavigation = async (services: Services) => {
|
|||
footerGroup.title = PROJECT_SETTINGS_TITLE;
|
||||
footerGroup.children.push({ cloudLink: 'billingAndSub', openInNewTab: true });
|
||||
}
|
||||
|
||||
applyAiSocNavigation(draft, productTypes);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -75,7 +75,7 @@ export class SecuritySolutionServerlessPlugin
|
|||
});
|
||||
|
||||
setOnboardingSettings(services);
|
||||
startNavigation(services);
|
||||
startNavigation(services, productTypes);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue