diff --git a/x-pack/solutions/security/packages/features/src/product_features_keys.ts b/x-pack/solutions/security/packages/features/src/product_features_keys.ts index f0acfa04bb00a..79fce4585abdc 100644 --- a/x-pack/solutions/security/packages/features/src/product_features_keys.ts +++ b/x-pack/solutions/security/packages/features/src/product_features_keys.ts @@ -117,6 +117,11 @@ export enum ProductFeatureSecurityKey { * Enables customization of prebuilt Elastic rules */ prebuiltRuleCustomization = 'prebuilt_rule_customization', + + /** + * Enables graph visualization for alerts and events + */ + graphVisualization = 'graph_visualization', } export enum ProductFeatureCasesKey { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_has_graph_visualization_access.test.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_has_graph_visualization_access.test.ts new file mode 100644 index 0000000000000..f20f74da9f99f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_has_graph_visualization_access.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { useHasGraphVisualizationAccess } from './use_has_graph_visualization_access'; +import { useKibana } from '../lib/kibana'; +import { useLicense } from './use_license'; +import { BehaviorSubject } from 'rxjs'; + +jest.mock('../lib/kibana'); +jest.mock('./use_license'); + +describe('useHasGraphVisualizationAccess', () => { + const mockUseKibana = useKibana as jest.Mock; + const mockUseLicense = useLicense as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('ESS/Self-Managed Environment', () => { + it('should return true when user has Enterprise license', () => { + const productFeatureKeys$ = new BehaviorSubject | null>(null); + + mockUseKibana.mockReturnValue({ + services: { + productFeatureKeys$, + serverless: undefined, + }, + }); + + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn(() => true), + }); + + const { result } = renderHook(() => useHasGraphVisualizationAccess()); + + expect(result.current).toBe(true); + }); + + it('should return false when user does not have Enterprise license', () => { + const productFeatureKeys$ = new BehaviorSubject | null>(null); + + mockUseKibana.mockReturnValue({ + services: { + productFeatureKeys$, + serverless: undefined, + }, + }); + + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn(() => false), + }); + + const { result } = renderHook(() => useHasGraphVisualizationAccess()); + + expect(result.current).toBe(false); + }); + }); + + describe('Serverless Environment', () => { + it('should return true when user has Complete tier with graphVisualization feature', () => { + const productFeatureKeys$ = new BehaviorSubject | null>( + new Set(['graph_visualization']) + ); + + mockUseKibana.mockReturnValue({ + services: { + productFeatureKeys$, + serverless: { + projectType: 'security', + }, + }, + }); + + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn(() => false), + }); + + const { result } = renderHook(() => useHasGraphVisualizationAccess()); + + expect(result.current).toBe(true); + }); + + it('should return false when user has Essentials tier without graphVisualization feature', () => { + const productFeatureKeys$ = new BehaviorSubject | null>(new Set([])); + + mockUseKibana.mockReturnValue({ + services: { + productFeatureKeys$, + serverless: { + projectType: 'security', + }, + }, + }); + + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn(() => false), + }); + + const { result } = renderHook(() => useHasGraphVisualizationAccess()); + + expect(result.current).toBe(false); + }); + + it('should return false when productFeatureKeys is null', () => { + const productFeatureKeys$ = new BehaviorSubject | null>(null); + + mockUseKibana.mockReturnValue({ + services: { + productFeatureKeys$, + serverless: { + projectType: 'security', + }, + }, + }); + + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn(() => true), + }); + + const { result } = renderHook(() => useHasGraphVisualizationAccess()); + + expect(result.current).toBe(false); + }); + + it('should prioritize PLI check over license check in serverless mode', () => { + const productFeatureKeys$ = new BehaviorSubject | null>(new Set([])); + + mockUseKibana.mockReturnValue({ + services: { + productFeatureKeys$, + serverless: { + projectType: 'security', + }, + }, + }); + + // Even though isEnterprise returns true, PLI check should take precedence + const isEnterpriseMock = jest.fn(() => true); + mockUseLicense.mockReturnValue({ + isEnterprise: isEnterpriseMock, + }); + + const { result } = renderHook(() => useHasGraphVisualizationAccess()); + + // Should return false because graphVisualization is not in PLI + expect(result.current).toBe(false); + + // isEnterprise should not be called in serverless mode + expect(isEnterpriseMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_has_graph_visualization_access.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_has_graph_visualization_access.ts new file mode 100644 index 0000000000000..877fadf6c3f19 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_has_graph_visualization_access.ts @@ -0,0 +1,38 @@ +/* + * 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 useObservable from 'react-use/lib/useObservable'; +import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys'; +import { useKibana } from '../lib/kibana'; +import { useLicense } from './use_license'; + +/** + * Hook to determine if the user has the required license for graph visualization feature. + * + * In ESS/Self-Managed: Requires Enterprise license or higher + * In Serverless: Requires Security Analytics Complete tier (not Essentials) + * + * @returns boolean indicating if graph visualization is available + */ +export const useHasGraphVisualizationAccess = (): boolean => { + const { productFeatureKeys$, serverless } = useKibana().services; + const licenseService = useLicense(); + + // Get current product feature keys from observable (serverless PLI system) + const productFeatureKeys = useObservable(productFeatureKeys$, null); + + // Detect if running in serverless mode + const isServerless = serverless !== undefined; + + if (isServerless) { + // In serverless: Check if Complete tier feature is enabled + return productFeatureKeys?.has(ProductFeatureSecurityKey.graphVisualization) ?? false; + } else { + // In ESS/Self-Managed: Check for Enterprise license or higher + return licenseService.isEnterprise(); + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.test.tsx index 5e4688b2a6b04..8bd7f21d0bfb2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { useExpandableFlyoutState } from '@kbn/expandable-flyout'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_ID } from '../components/graph_visualization'; @@ -22,7 +21,6 @@ const mockSessionViewTestId = 'session-view'; // Mock all required dependencies jest.mock('../../shared/hooks/use_graph_preview'); -jest.mock('@kbn/kibana-react-plugin/public'); jest.mock('@kbn/expandable-flyout'); jest.mock('../../shared/context'); jest.mock('../components/graph_visualization', () => ({ @@ -83,8 +81,10 @@ describe('VisualizeTab', () => { }); }); - it('should not render GraphVisualization component when feature flag is disabled', () => { - (useUiSetting$ as jest.Mock).mockImplementation((setting) => [false]); + it('should not render GraphVisualization component when graph is not available', () => { + (useGraphPreview as jest.Mock).mockReturnValue({ + hasGraphRepresentation: false, + }); renderVisualizeTab(); @@ -95,8 +95,10 @@ describe('VisualizeTab', () => { expect(screen.getByTestId(mockSessionViewTestId)).toBeInTheDocument(); }); - it('should render GraphVisualization component when feature flag is enabled', () => { - (useUiSetting$ as jest.Mock).mockImplementation((setting) => [true]); + it('should render GraphVisualization component when graph is available', () => { + (useGraphPreview as jest.Mock).mockReturnValue({ + hasGraphRepresentation: true, + }); renderVisualizeTab(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index d72df8ab67ec8..47a0f57e4aa0a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -15,7 +15,6 @@ import { uiMetricService, GRAPH_INVESTIGATION, } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; -import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { useDocumentDetailsContext } from '../../shared/context'; import { VISUALIZE_TAB_BUTTON_GROUP_TEST_ID, @@ -30,7 +29,6 @@ import { useStartTransaction } from '../../../../common/lib/apm/use_start_transa import { GRAPH_ID, GraphVisualization } from '../components/graph_visualization'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { METRIC_TYPE } from '../../../../common/lib/telemetry'; -import { ENABLE_GRAPH_VISUALIZATION_SETTING } from '../../../../../common/constants'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ { @@ -113,11 +111,9 @@ export const VisualizeTab = memo(() => { dataFormattedForFieldBrowser, }); - const [graphVisualizationEnabled] = useUiSetting$(ENABLE_GRAPH_VISUALIZATION_SETTING); - const options = [...visualizeButtons]; - if (hasGraphRepresentation && graphVisualizationEnabled) { + if (hasGraphRepresentation) { options.push(graphVisualizationButton); } @@ -125,8 +121,8 @@ export const VisualizeTab = memo(() => { if (panels.left?.path?.subTab) { const newId = panels.left.path.subTab; - // Check if we need to select a different tab when graph feature flag is disabled - if (newId === GRAPH_ID && hasGraphRepresentation && !graphVisualizationEnabled) { + // Check if we need to select a different tab when graph is not available + if (newId === GRAPH_ID && !hasGraphRepresentation) { setActiveVisualizationId(SESSION_VIEW_ID); } else { setActiveVisualizationId(newId); @@ -136,7 +132,7 @@ export const VisualizeTab = memo(() => { } } } - }, [panels.left?.path?.subTab, graphVisualizationEnabled, hasGraphRepresentation]); + }, [panels.left?.path?.subTab, hasGraphRepresentation]); return ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 816806ab173e5..afdd9ba7c129c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -113,8 +113,9 @@ describe('', () => { alertIds: undefined, statsNodes: undefined, }); + // Default mock: graph visualization not available (UI setting is false by default) mockUseGraphPreview.mockReturnValue({ - hasGraphRepresentation: true, + hasGraphRepresentation: false, eventIds: [], }); mockUseFetchGraphData.mockReturnValue({ @@ -178,6 +179,12 @@ describe('', () => { return useUiSetting$Mock(key, defaultValue); }); + // Mock useGraphPreview to reflect that graph is available when UI setting is enabled + mockUseGraphPreview.mockReturnValue({ + hasGraphRepresentation: true, + eventIds: [], + }); + const { getByTestId } = renderVisualizationsSection(); expect(getByTestId(`${GRAPH_PREVIEW_TEST_ID}LeftSection`)).toBeInTheDocument(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index 9dc0e97a842c3..26b3368de6ac1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { useExpandSection } from '../hooks/use_expand_section'; import { AnalyzerPreviewContainer } from './analyzer_preview_container'; import { SessionPreviewContainer } from './session_preview_container'; @@ -17,7 +16,6 @@ import { VISUALIZATIONS_TEST_ID } from './test_ids'; import { GraphPreviewContainer } from './graph_preview_container'; import { useDocumentDetailsContext } from '../../shared/context'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { ENABLE_GRAPH_VISUALIZATION_SETTING } from '../../../../../common/constants'; const KEY = 'visualizations'; @@ -29,8 +27,6 @@ export const VisualizationsSection = memo(() => { const { dataAsNestedObject, getFieldsData, dataFormattedForFieldBrowser } = useDocumentDetailsContext(); - const [graphVisualizationEnabled] = useUiSetting$(ENABLE_GRAPH_VISUALIZATION_SETTING); - // Decide whether to show the graph preview or not const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, @@ -38,11 +34,6 @@ export const VisualizationsSection = memo(() => { dataFormattedForFieldBrowser, }); - const shouldShowGraphPreview = useMemo( - () => graphVisualizationEnabled && hasGraphRepresentation, - [graphVisualizationEnabled, hasGraphRepresentation] - ); - return ( { - {shouldShowGraphPreview && ( + {hasGraphRepresentation && ( <> diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx index cf1ee82078395..2e94abaa7c647 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx @@ -12,6 +12,14 @@ import { useGraphPreview } from './use_graph_preview'; import type { GetFieldsData } from './use_get_fields_data'; import { mockFieldData } from '../mocks/mock_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; +import { useHasGraphVisualizationAccess } from '../../../../common/hooks/use_has_graph_visualization_access'; + +jest.mock('../../../../common/hooks/use_has_graph_visualization_access'); +const mockUseHasGraphVisualizationAccess = useHasGraphVisualizationAccess as jest.Mock; + +jest.mock('@kbn/kibana-react-plugin/public'); +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; +const mockUseUiSetting = useUiSetting$ as jest.Mock; const alertMockGetFieldsData: GetFieldsData = (field: string) => { if (field === 'kibana.alert.uuid') { @@ -48,6 +56,13 @@ const eventMockGetFieldsData: GetFieldsData = (field: string) => { const eventMockDataFormattedForFieldBrowser: TimelineEventsDetailsItem[] = []; describe('useGraphPreview', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Default mock: graph visualization feature is available + mockUseHasGraphVisualizationAccess.mockReturnValue(true); + // Default mock: UI setting is enabled + mockUseUiSetting.mockReturnValue([true, jest.fn()]); + }); it(`should return false when missing actor`, () => { const getFieldsData: GetFieldsData = (field: string) => { if (field === 'actor.entity.id') { @@ -326,4 +341,51 @@ describe('useGraphPreview', () => { isAlert: true, }); }); + + it('should return false when all conditions are met but env does not have required license', () => { + mockUseHasGraphVisualizationAccess.mockReturnValue(false); + + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData: alertMockGetFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action'], + }, + }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, + }, + }); + + expect(hookResult.result.current.hasGraphRepresentation).toBe(false); + expect(hookResult.result.current).toStrictEqual({ + hasGraphRepresentation: false, + timestamp: mockFieldData['@timestamp'][0], + eventIds: ['eventId'], + actorIds: ['actorId'], + action: ['action'], + targetIds: ['targetId'], + isAlert: true, + }); + }); + + it('should return false for hasGraphRepresentation when UI setting is disabled', () => { + mockUseUiSetting.mockReturnValue([false, jest.fn()]); + + const hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData: alertMockGetFieldsData, + ecsData: { + _id: 'id', + event: { + action: ['action'], + }, + }, + dataFormattedForFieldBrowser: alertMockDataFormattedForFieldBrowser, + }, + }); + + expect(hookResult.result.current.hasGraphRepresentation).toBe(false); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts index 8f05b87844fb2..743574e3e4f4c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts @@ -8,9 +8,12 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { get } from 'lodash/fp'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import type { GetFieldsData } from './use_get_fields_data'; import { getField, getFieldArray } from '../utils'; import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; +import { useHasGraphVisualizationAccess } from '../../../../common/hooks/use_has_graph_visualization_access'; +import { ENABLE_GRAPH_VISUALIZATION_SETTING } from '../../../../../common/constants'; export interface UseGraphPreviewParams { /** @@ -58,7 +61,8 @@ export interface UseGraphPreviewResult { action?: string[]; /** - * Boolean indicating if the event is has a graph representation (contains event ids, actor ids and action) + * Boolean indicating if graph visualization is fully available + * Combines: data availability (event ids, actor ids and action) + valid license + feature enabled in settings */ hasGraphRepresentation: boolean; @@ -84,13 +88,33 @@ export const useGraphPreview = ({ const actorIds = getFieldArray(getFieldsData('actor.entity.id')); const targetIds = getFieldArray(getFieldsData('target.entity.id')); const action: string[] | undefined = get(['event', 'action'], ecsData); - const hasGraphRepresentation = + + // Check if user license is high enough to access graph visualization + const hasRequiredLicense = useHasGraphVisualizationAccess(); + + // Check if graph visualization feature is enabled in UI settings + const [isGraphFeatureEnabled] = useUiSetting$(ENABLE_GRAPH_VISUALIZATION_SETTING); + + // Check if graph has all required data fields for graph visualization + const hasGraphData = Boolean(timestamp) && Boolean(action?.length) && actorIds.length > 0 && eventIds.length > 0 && targetIds.length > 0; + + // Combine all conditions: data availability + license + feature flag + const hasGraphRepresentation = hasGraphData && hasRequiredLicense && isGraphFeatureEnabled; + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); - return { timestamp, eventIds, actorIds, action, targetIds, hasGraphRepresentation, isAlert }; + return { + timestamp, + eventIds, + actorIds, + action, + targetIds, + hasGraphRepresentation, + isAlert, + }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/mocks.ts b/x-pack/solutions/security/plugins/security_solution/public/mocks.ts index 2cf1ad86b12f6..8122536b5875a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/mocks.ts @@ -7,6 +7,7 @@ import { BehaviorSubject, of } from 'rxjs'; import { UpsellingService } from '@kbn/security-solution-upselling/service'; +import type { ProductFeatureKeyType } from '@kbn/security-solution-features'; import type { BreadcrumbsNav } from './common/breadcrumbs'; import { allowedExperimentalValues } from '../common/experimental_features'; import type { PluginStart, PluginSetup, ContractStartServices } from './types'; @@ -19,6 +20,7 @@ export const contractStartServicesMock: ContractStartServices = { getComponents$: jest.fn(() => of({})), upselling, onboarding: onboardingService, + productFeatureKeys$: new BehaviorSubject | null>(null), }; const setupMock = (): PluginSetup => ({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/plugin_contract.ts b/x-pack/solutions/security/plugins/security_solution/public/plugin_contract.ts index a279164c09b5d..415167800f086 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/plugin_contract.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/plugin_contract.ts @@ -62,6 +62,7 @@ export class PluginContract { getComponents$: this.componentsService.getComponents$.bind(this.componentsService), upselling: this.upsellingService, onboarding: this.onboardingService, + productFeatureKeys$: this.productFeatureKeys$, }; } } diff --git a/x-pack/solutions/security/plugins/security_solution/public/types.ts b/x-pack/solutions/security/plugins/security_solution/public/types.ts index 1094162150c77..2c1e44cf36f99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/types.ts @@ -62,7 +62,7 @@ import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import type { AutomaticImportPluginStart } from '@kbn/automatic-import-plugin/public'; -import type { ProductFeatureKeys } from '@kbn/security-solution-features'; +import type { ProductFeatureKeyType, ProductFeatureKeys } from '@kbn/security-solution-features'; import type { ElasticAssistantSharedStatePublicPluginStart } from '@kbn/elastic-assistant-shared-state-plugin/public'; import type { InferencePublicStart } from '@kbn/inference-plugin/public'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; @@ -178,6 +178,7 @@ export interface ContractStartServices { getComponents$: GetComponents$; upselling: UpsellingService; onboarding: OnboardingService; + productFeatureKeys$: Observable | null>; } export type StartServices = CoreStart & diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts index b5333b6a75774..d17f71d018c1b 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -58,6 +58,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = { ProductFeatureKey.prebuiltRuleCustomization, ProductFeatureKey.siemMigrations, ProductFeatureKey.aiValueReport, + ProductFeatureKey.graphVisualization, ], }, [ProductLine.endpoint]: {