From a5a7173e7b5f17e93325ddf293f1eb547360341b Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Wed, 19 Nov 2025 14:42:18 +0100 Subject: [PATCH 01/34] declare the share plugin as a security_solution dependency --- .../security/plugins/security_solution/kibana.jsonc | 1 + .../security_solution/public/common/lib/kibana/services.ts | 4 +++- .../plugins/security_solution/public/reports/links.ts | 6 +++++- .../security/plugins/security_solution/public/types.ts | 3 +++ .../plugins/security_solution/server/plugin_contract.ts | 3 ++- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc index adea078d6c9b8..e0d1f97097967 100644 --- a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc +++ b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc @@ -62,6 +62,7 @@ "productDocBase", "telemetry", "elasticAssistantSharedState", + "share" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/services.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/services.ts index 4a50efe98910a..9dcfef20629fa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/services.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/services.ts @@ -9,7 +9,7 @@ import type { CoreStart } from '@kbn/core/public'; import type { StartPlugins } from '../../../types'; type GlobalServices = Pick & - Pick; + Pick; /** * This class is a singleton that holds references to core Kibana services. @@ -46,6 +46,7 @@ export class KibanaServices { notifications, expressions, savedSearch, + share, }: GlobalServices & { kibanaBranch: string; kibanaVersion: string; @@ -61,6 +62,7 @@ export class KibanaServices { notifications, expressions, savedSearch, + share, }; this.kibanaBranch = kibanaBranch; this.kibanaVersion = kibanaVersion; diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/links.ts b/x-pack/solutions/security/plugins/security_solution/public/reports/links.ts index fe49506971a16..b42de0507ed05 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/links.ts @@ -33,6 +33,10 @@ export const aiValueLinks: LinkItem = { i18n.translate('xpack.securitySolution.appLinks.aiValue', { defaultMessage: 'AI Value', }), + i18n.translate('xpack.securitySolution.appLinks.valueReport', { + defaultMessage: 'Value report', + }), ], - globalNavPosition: 8, + globalNavPosition: 12, + hideTimeline: true, }; 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 fd907fe349ea1..584572e88e3e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/types.ts @@ -64,6 +64,7 @@ import type { AutomaticImportPluginStart } from '@kbn/automatic-import-plugin/pu import type { 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'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { Detections } from './detections'; @@ -101,6 +102,7 @@ import type { SiemMigrationsService } from './siem_migrations/service'; export interface SetupPlugins { cloud?: CloudSetup; home?: HomePublicPluginSetup; + share?: SharePluginSetup; licensing: LicensingPluginSetup; management: ManagementSetup; security: SecurityPluginSetup; @@ -161,6 +163,7 @@ export interface StartPlugins { productDocBase: ProductDocBasePluginStart; elasticAssistantSharedState: ElasticAssistantSharedStatePublicPluginStart; inference: InferencePublicStart; + share?: SharePluginStart; } export interface StartPluginsDependencies extends StartPlugins { diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts index ffbd6cc5ea0b9..7a97fda24b05d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts @@ -41,7 +41,7 @@ import type { import type { TelemetryPluginStart, TelemetryPluginSetup } from '@kbn/telemetry-plugin/server'; import type { OsqueryPluginSetup } from '@kbn/osquery-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; -import type { SharePluginStart } from '@kbn/share-plugin/server'; +import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/server'; import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server'; import type { ElasticAssistantPluginStart } from '@kbn/elastic-assistant-plugin/server'; import type { InferenceServerStart } from '@kbn/inference-plugin/server'; @@ -68,6 +68,7 @@ export interface SecuritySolutionPluginSetupDependencies { licensing: LicensingPluginSetup; osquery: OsqueryPluginSetup; unifiedSearch: UnifiedSearchServerPluginSetup; + share?: SharePluginSetup; } export interface SecuritySolutionPluginStartDependencies { From cf2792535e270268f72e1a8430cbe98576290787 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Wed, 19 Nov 2025 14:50:02 +0100 Subject: [PATCH 02/34] allow exporting ai value reports in ess --- .../private/kbn-reporting/common/constants.ts | 2 + .../public/share/integrations/pdf/index.ts | 2 +- .../shared/deeplinks/analytics/constants.ts | 2 + .../shared/deeplinks/analytics/index.ts | 1 + ...cheduled_report_share_integration.test.tsx | 10 + .../scheduled_report_share_integration.tsx | 6 +- .../security_solution/common/constants.ts | 1 + .../locators/ai_value_report/locator.test.ts | 73 ++++++++ .../locators/ai_value_report/locator.ts | 47 +++++ .../security_side_nav/categories.ts | 4 + .../use_init_timerange_url_params.test.tsx | 24 +-- .../use_init_timerange_url_params.ts | 19 +- .../use_sync_timerange_url_param.ts | 8 +- .../security_solution/public/plugin.tsx | 6 +- .../cost_savings_key_insight.test.tsx | 102 ++++++++++ .../ai_value/cost_savings_key_insight.tsx | 59 +++++- .../ai_value/cost_savings_metric.test.tsx | 37 ++++ .../ai_value/cost_savings_metric.tsx | 27 ++- .../components/ai_value/index.test.tsx | 44 +++++ .../reports/components/ai_value/index.tsx | 44 ++++- .../use_download_ai_value_report.test.tsx | 174 ++++++++++++++++++ .../hooks/use_download_ai_value_report.tsx | 85 +++++++++ .../public/reports/pages/ai_value.tsx | 96 +++++++--- .../ai_value/export_provider.test.tsx | 172 +++++++++++++++++ .../providers/ai_value/export_provider.tsx | 125 +++++++++++++ .../security_solution/server/plugin.ts | 5 + .../security_solution/server/ui_settings.ts | 1 + .../server/plugin.ts | 6 - 28 files changed, 1086 insertions(+), 96 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx diff --git a/src/platform/packages/private/kbn-reporting/common/constants.ts b/src/platform/packages/private/kbn-reporting/common/constants.ts index f135c7a4a9907..68e0eeb7a1969 100644 --- a/src/platform/packages/private/kbn-reporting/common/constants.ts +++ b/src/platform/packages/private/kbn-reporting/common/constants.ts @@ -13,6 +13,7 @@ import { DASHBOARD_APP_LOCATOR, LENS_APP_LOCATOR, VISUALIZE_APP_LOCATOR, + AI_VALUE_REPORT_LOCATOR, } from '@kbn/deeplinks-analytics'; import type { LicenseType } from '@kbn/licensing-types'; @@ -68,6 +69,7 @@ export const REPORTING_REDIRECT_ALLOWED_LOCATOR_TYPES = [ DASHBOARD_APP_LOCATOR, LENS_APP_LOCATOR, VISUALIZE_APP_LOCATOR, + AI_VALUE_REPORT_LOCATOR, ]; // Redirection URL used to load app state for screenshotting diff --git a/src/platform/packages/private/kbn-reporting/public/share/integrations/pdf/index.ts b/src/platform/packages/private/kbn-reporting/public/share/integrations/pdf/index.ts index 9661a52cd7d1b..1826896eac30b 100644 --- a/src/platform/packages/private/kbn-reporting/public/share/integrations/pdf/index.ts +++ b/src/platform/packages/private/kbn-reporting/public/share/integrations/pdf/index.ts @@ -16,7 +16,7 @@ export const reportingPDFExportShareIntegration = ({ apiClient, startServices$, }: ExportModalShareOpts): RegisterShareIntegrationArgs => { - const supportedObjectTypes = ['dashboard', 'visualization', 'lens']; + const supportedObjectTypes = ['dashboard', 'visualization', 'lens', 'ai_value_report']; return { id: 'pdfReports', diff --git a/src/platform/packages/shared/deeplinks/analytics/constants.ts b/src/platform/packages/shared/deeplinks/analytics/constants.ts index e2ff238704e68..c8886a500b66a 100644 --- a/src/platform/packages/shared/deeplinks/analytics/constants.ts +++ b/src/platform/packages/shared/deeplinks/analytics/constants.ts @@ -32,3 +32,5 @@ export const DASHBOARD_SAVED_OBJECT_TYPE = 'dashboard'; export const LENS_APP_LOCATOR = 'LENS_APP_LOCATOR'; export const VISUALIZE_APP_LOCATOR = 'VISUALIZE_APP_LOCATOR'; + +export const AI_VALUE_REPORT_LOCATOR = 'AI_VALUE_REPORT_LOCATOR'; diff --git a/src/platform/packages/shared/deeplinks/analytics/index.ts b/src/platform/packages/shared/deeplinks/analytics/index.ts index 14ae160026e55..c306b228ce5cc 100644 --- a/src/platform/packages/shared/deeplinks/analytics/index.ts +++ b/src/platform/packages/shared/deeplinks/analytics/index.ts @@ -17,6 +17,7 @@ export { LENS_APP_LOCATOR, DISCOVER_ESQL_LOCATOR, DASHBOARD_APP_LOCATOR, + AI_VALUE_REPORT_LOCATOR, } from './constants'; export type { AppId, DeepLinkId } from './deep_links'; diff --git a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx index 21bacee9c0742..68229b681159e 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx @@ -113,6 +113,16 @@ describe('createScheduledReportShareIntegration', () => { }) ).toBe(false); }); + + it('should return false for unsupported object types', () => { + expect( + integration.prerequisiteCheck!({ + license: { type: SCHEDULED_REPORT_VALID_LICENSES[0] } as ILicense, + capabilities, + objectType: 'ai_value_report', + }) + ).toBe(false); + }); }); describe('config.shouldRender', () => { diff --git a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx index 588a8330538e9..3a2c93e02e740 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx @@ -29,6 +29,8 @@ export interface CreateScheduledReportProviderOptions { services: ReportingPublicPluginStartDependencies; } +const unsupportedObjectTypes = ['ai_value_report']; + export const shouldRegisterScheduledReportShareIntegration = async (http: HttpSetup) => { const { isSufficientlySecure, hasPermanentEncryptionKey } = await queryClient.fetchQuery({ queryKey: getReportingHealthQueryKey(), @@ -75,8 +77,8 @@ export const createScheduledReportShareIntegration = ({ flyoutSizing: { size: 'm', maxWidth: 500 }, }; }, - prerequisiteCheck: ({ license }) => { - if (!license || !license.type) { + prerequisiteCheck: ({ license, objectType }) => { + if (!license || !license.type || unsupportedObjectTypes.includes(objectType)) { return false; } return SCHEDULED_REPORT_VALID_LICENSES.includes(license.type); diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index c7a57adad554f..6b3777959d86a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -54,6 +54,7 @@ export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const; export const DEFAULT_LISTS_INDEX = '.lists' as const; export const DEFAULT_ITEMS_INDEX = '.items' as const; export const DEFAULT_RISK_SCORE_PAGE_SIZE = 1000 as const; +export const AI_VALUE_REPORT_LOCATOR = 'AI_VALUE_REPORT_LOCATOR' as const; // The DEFAULT_MAX_SIGNALS value exists also in `x-pack/platform/plugins/shared/cases/common/constants.ts` // If either changes, engineer should ensure both values are updated export const DEFAULT_MAX_SIGNALS = 100 as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts new file mode 100644 index 0000000000000..a8cdae3afccd6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { AIValueReportLocatorDefinition, parseLocationState } from './locator'; +import { AI_VALUE_REPORT_LOCATOR, AI_VALUE_PATH, APP_UI_ID } from '../../constants'; + +describe('AIValueReportLocatorDefinition', () => { + const locator = new AIValueReportLocatorDefinition(); + + const validParams = { + timeRange: { + from: '2024-01-01T00:00:00Z', + to: '2024-01-02T00:00:00Z', + }, + insight: 'Some valuable insight!', + reportDataHash: 'abc123', + }; + + test('id should match constant', () => { + expect(locator.id).toBe(AI_VALUE_REPORT_LOCATOR); + }); + + test('getLocation returns correct location object', async () => { + const result = await locator.getLocation(validParams); + + expect(result).toEqual({ + app: APP_UI_ID, + path: AI_VALUE_PATH, + state: validParams, + }); + }); +}); + +describe('parseLocationState', () => { + const validState = { + timeRange: { + from: '2024-01-01T00:00:00Z', + to: '2024-01-01T00:00:00Z', + }, + insight: 'Some valuable insight!', + reportDataHash: 'hash123', + }; + + it('returns parsed state when valid', () => { + const result = parseLocationState(validState); + expect(result).toEqual(validState); + }); + + it('strips unknown fields but preserves valid ones', () => { + const stateWithExtras = { + ...validState, + extraField: 'foo', + anotherOne: 42, + }; + + const result = parseLocationState(stateWithExtras); + + expect(result).toEqual(stateWithExtras); + }); + + it('returns undefined for invalid state (missing fields)', () => { + const invalid = { + insight: 'missing timeRange and hash', + }; + + const result = parseLocationState(invalid); + expect(result).toBeUndefined(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts new file mode 100644 index 0000000000000..2bc5ee70ed9d4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts @@ -0,0 +1,47 @@ +/* + * 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 { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; +import { z } from '@kbn/zod'; +import { AI_VALUE_REPORT_LOCATOR, AI_VALUE_PATH, APP_UI_ID } from '../../constants'; + +const AIValueReportParamsSchema = z.object({ + timeRange: z.object({ + to: z.string().nonempty(), + from: z.string().nonempty(), + }), + insight: z.string().nonempty(), + reportDataHash: z.string().nonempty(), +}); + +export type AIValueReportParams = z.infer; + +export type ForwardedAIValueReportState = AIValueReportParams; + +export type AIValueReportLocator = LocatorPublic; + +export class AIValueReportLocatorDefinition implements LocatorDefinition { + public readonly id = AI_VALUE_REPORT_LOCATOR; + + public readonly getLocation = async (params: AIValueReportParams) => { + return { + app: APP_UI_ID, + path: AI_VALUE_PATH, + state: params, + }; + }; +} + +export const parseLocationState = (state: unknown): ForwardedAIValueReportState | undefined => { + const result = AIValueReportParamsSchema.passthrough().safeParse(state); + if (result.error) { + // This will cause the page to fallback to rendering normally + return undefined; + } + + return result.data; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/navigation/security_side_nav/categories.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/navigation/security_side_nav/categories.ts index 4450e3ac35bb5..1ceb9e33069e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/navigation/security_side_nav/categories.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/navigation/security_side_nav/categories.ts @@ -47,5 +47,9 @@ export const getNavCategories = ( type: LinkCategoryType.separator, linkIds: [SecurityPageName.siemReadiness], }, + { + type: LinkCategoryType.separator, + linkIds: [SecurityPageName.aiValue], + }, ]; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.test.tsx index 0f107e27c24b5..f200e4250d1a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.test.tsx @@ -11,7 +11,6 @@ import * as redux from 'react-redux'; import * as experimentalFeatures from '../use_experimental_features'; import * as globalQueryString from '../../utils/global_query_string'; import { TestProviders } from '../../mock'; -import { useKibana } from '../../lib/kibana'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -27,12 +26,6 @@ describe('useInitTimerangeFromUrlParam', () => { jest.clearAllMocks(); (redux.useDispatch as jest.Mock).mockReturnValue(dispatch); (experimentalFeatures.useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); - (useKibana as jest.Mock).mockReturnValue({ - services: { - serverless: {}, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); }); it('should call useInitializeUrlParam with correct params', () => { @@ -45,20 +38,7 @@ describe('useInitTimerangeFromUrlParam', () => { ); }); - it('should call dispatch 2 times on init url params when not serverless', () => { - (useKibana as jest.Mock).mockReturnValue({ - services: {}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - renderHook(() => useInitTimerangeFromUrlParam(), { - wrapper: TestProviders, - }); - const callback = (globalQueryString.useInitializeUrlParam as jest.Mock).mock.calls[0][1]; - callback({ valueReport: { timerange: { kind: 'absolute' } } }); - expect(dispatch).toHaveBeenCalledTimes(2); - }); - - it('should call dispatch 3 times on init url params when serverless and valueReport exists', () => { + it('should call dispatch 3 times on init url params when valueReport exists', () => { renderHook(() => useInitTimerangeFromUrlParam(), { wrapper: TestProviders, }); @@ -67,7 +47,7 @@ describe('useInitTimerangeFromUrlParam', () => { expect(dispatch).toHaveBeenCalledTimes(3); }); - it('should call dispatch 6 times on init url params when serverless, valueReport exists, and isSocTrendsEnabled=true', () => { + it('should call dispatch 6 times on init url params when valueReport exists, and isSocTrendsEnabled=true', () => { (experimentalFeatures.useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); renderHook(() => useInitTimerangeFromUrlParam(), { wrapper: TestProviders, diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts index 2c8a3ad725ee7..0b23dff666b4b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_init_timerange_url_params.ts @@ -9,7 +9,6 @@ import { useCallback } from 'react'; import { get, isEmpty } from 'lodash/fp'; import { useDispatch } from 'react-redux'; import type { Dispatch } from 'redux'; -import { useKibana } from '../../lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; import type { TimeRangeKinds } from '../../store/inputs/constants'; import type { @@ -28,18 +27,11 @@ import { InputsModelId } from '../../store/inputs/constants'; export const useInitTimerangeFromUrlParam = () => { const dispatch = useDispatch(); const isSocTrendsEnabled = useIsExperimentalFeatureEnabled('socTrendsEnabled'); - const { serverless } = useKibana().services; - // only on serverless - const isValueReportEnabled = !!serverless; + const onInitialize = useCallback( (initialState: UrlInputsModel | null) => - initializeTimerangeFromUrlParam( - initialState, - dispatch, - isSocTrendsEnabled, - isValueReportEnabled - ), - [dispatch, isSocTrendsEnabled, isValueReportEnabled] + initializeTimerangeFromUrlParam(initialState, dispatch, isSocTrendsEnabled), + [dispatch, isSocTrendsEnabled] ); useInitializeUrlParam(URL_PARAM_KEY.timerange, onInitialize); @@ -48,8 +40,7 @@ export const useInitTimerangeFromUrlParam = () => { const initializeTimerangeFromUrlParam = ( initialState: UrlInputsModel | null, dispatch: Dispatch, - isSocTrendsEnabled: boolean, - isValueReportEnabled: boolean + isSocTrendsEnabled: boolean ) => { if (initialState != null) { const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', initialState) }; @@ -188,7 +179,7 @@ const initializeTimerangeFromUrlParam = ( ); } } - if (valueReportType && isValueReportEnabled) { + if (valueReportType) { if (valueReportType === 'absolute') { const absoluteRange = normalizeTimeRange( get('valueReport.timerange', initialState) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts index dbb4b343e0772..b52f249808795 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts @@ -6,7 +6,6 @@ */ import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { useKibana } from '../../lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; import type { UrlInputsModel } from '../../store/inputs/model'; import { inputsSelectors } from '../../store/inputs'; @@ -18,9 +17,6 @@ export const useSyncTimerangeUrlParam = () => { const getInputSelector = useMemo(() => inputsSelectors.inputsSelector(), []); const inputState = useSelector(getInputSelector); const isSocTrendsEnabled = useIsExperimentalFeatureEnabled('socTrendsEnabled'); - const { serverless } = useKibana().services; - // only on serverless - const isValueReportEnabled = !!serverless; const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; @@ -39,7 +35,7 @@ export const useSyncTimerangeUrlParam = () => { }, [inputState.socTrends, isSocTrendsEnabled]); const valueReportUrlParams = useMemo(() => { - if (isValueReportEnabled && inputState.valueReport) { + if (inputState.valueReport) { const { linkTo: valueReportLinkTo, timerange: valueReportTimerange } = inputState.valueReport; return { valueReport: { @@ -49,7 +45,7 @@ export const useSyncTimerangeUrlParam = () => { }; } return {}; - }, [inputState.valueReport, isValueReportEnabled]); + }, [inputState.valueReport]); useEffect(() => { updateTimerangeUrlParam({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx b/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx index e7a5d48785acc..6b5b90f157ee1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx @@ -66,6 +66,7 @@ import { getExternalReferenceAttachmentEndpointRegular } from './cases/attachmen import { isSecuritySolutionAccessible } from './helpers_access'; import { generateAttachmentType } from './threat_intelligence/modules/cases/utils/attachments'; import { defaultDeepLinks } from './app/links/default_deep_links'; +import { AIValueReportLocatorDefinition } from '../common/locators/ai_value_report/locator'; export class Plugin implements IPlugin { private config: SecuritySolutionUiConfigType; @@ -104,8 +105,11 @@ export class Plugin implements IPlugin { diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx index ed349018488ea..f0b4a7fdc9860 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx @@ -21,6 +21,7 @@ import { MessageRole } from '@kbn/inference-common'; import type { VisualizationTablesWithMeta } from '../../../common/components/visualization_actions/types'; import type { StartServices } from '../../../types'; import { QueryClient, QueryClientProvider } from '@kbn/react-query'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; jest.mock('../../../common/lib/kibana', () => ({ useKibana: jest.fn(), @@ -51,9 +52,15 @@ jest.mock('../../../common/hooks/use_ai_connectors', () => ({ useAIConnectors: jest.fn(), })); +jest.mock('../../providers/ai_value/export_provider', () => ({ + useAIValueExportContext: jest.fn(), +})); + const mockUseKibana = useKibana as jest.Mock; const mockLicenseService = licenseService as jest.Mocked; const mockUseAssistantAvailability = useAssistantAvailability as jest.Mock; +const mockUseAIValueExportContext = useAIValueExportContext as jest.Mock; +const mockSetInsightInExportContext = jest.fn(); const mockUseFindCostSavingsPrompts = useFindCostSavingsPrompts as jest.MockedFunction< typeof useFindCostSavingsPrompts >; @@ -115,6 +122,9 @@ describe('CostSavingsKeyInsight', () => { }); beforeEach(() => { jest.clearAllMocks(); + mockUseAIValueExportContext.mockReturnValue({ + setInsight: mockSetInsightInExportContext, + }); mockUseKibana.mockReturnValue(createMockKibanaServices()); mockLicenseService.isEnterprise.mockReturnValue(true); @@ -168,6 +178,15 @@ describe('CostSavingsKeyInsight', () => { toasts: expect.any(Object), }, }); + expect(mockUseAIValueExportContext).toHaveBeenCalled(); + }); + }); + + it("sets the insight in the AI Value Export context after it's fetched", async () => { + render(, { wrapper }); + + await waitFor(() => { + expect(mockSetInsightInExportContext).toHaveBeenCalledWith(''); }); }); @@ -286,4 +305,87 @@ describe('CostSavingsKeyInsight', () => { expect(mockChatComplete).toHaveBeenCalledTimes(2); }); }); + + describe('export mode', () => { + const baseMockedContext = { + forwardedState: { + insight: chatCompleteResult, + }, + setInsight: mockSetInsightInExportContext, + isInsightVerified: false, + shouldRegenerateInsight: undefined, + }; + let rerender: (ui: React.ReactNode) => void; + beforeEach(() => { + mockUseAIValueExportContext.mockReturnValue(baseMockedContext); + const renderResult = render(, { wrapper }); + rerender = renderResult.rerender; + }); + + it('should show the loading component when the insight has not been verified', () => { + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + describe('when the insight in the forwarded state can be used', () => { + beforeEach(() => { + mockUseAIValueExportContext.mockReturnValue({ + ...baseMockedContext, + isInsightVerified: true, + shouldRegenerateInsight: false, + }); + + rerender(); + }); + + it('should not attempt to generate the insight', () => { + expect(mockUseKibana).not.toHaveBeenCalled(); + expect(mockUseFindCostSavingsPrompts).not.toHaveBeenCalled(); + expect(mockLicenseService.isEnterprise).not.toHaveBeenCalled(); + expect(mockUseAssistantAvailability).not.toHaveBeenCalled(); + }); + + it('should display the insight', () => { + expect(screen.getByText(chatCompleteResult)).toBeInTheDocument(); + }); + }); + + describe('when the insight should be regenerated', () => { + beforeEach(() => { + mockUseAIValueExportContext.mockReturnValue({ + ...baseMockedContext, + isInsightVerified: true, + shouldRegenerateInsight: true, + }); + + rerender(); + }); + + it('should attempt to generate the insight', async () => { + await waitFor(() => { + expect(screen.getByTestId('alertProcessingKeyInsightsContainer')).toBeInTheDocument(); + expect(screen.getByTestId('alertProcessingKeyInsightsGreetingGroup')).toBeInTheDocument(); + expect(screen.getByTestId('alertProcessingKeyInsightsLogo')).toBeInTheDocument(); + expect(screen.getByTestId('alertProcessingKeyInsightsGreeting')).toBeInTheDocument(); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect(mockUseKibana).toHaveBeenCalled(); + expect(mockLicenseService.isEnterprise).toHaveBeenCalled(); + expect(mockUseAssistantAvailability).toHaveBeenCalled(); + expect(mockUseFindCostSavingsPrompts).toHaveBeenCalledWith({ + context: { + isAssistantEnabled: true, + httpFetch: expect.any(Function), + toasts: expect.any(Object), + }, + }); + expect(mockUseAIValueExportContext).toHaveBeenCalled(); + }); + }); + + it('should display the insight', async () => { + await waitFor(() => { + expect(screen.getByText(chatCompleteResult)).toBeInTheDocument(); + }); + }); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx index e221c4f6eec5e..c1d16b4df71c5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx @@ -25,27 +25,26 @@ import { licenseService } from '../../../common/hooks/use_license'; import { useAssistantAvailability } from '../../../assistant/use_assistant_availability'; import { useFindCostSavingsPrompts } from '../../hooks/use_find_cost_savings_prompts'; import { useDefaultAIConnectorId } from '../../../common/hooks/use_default_ai_connector_id'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { isLoading: boolean; lensResponse: VisualizationTablesWithMeta | null; } -export const CostSavingsKeyInsight: React.FC = ({ isLoading, lensResponse }) => { - const { - euiTheme: { size }, - } = useEuiTheme(); - +const CostSavingsKeyInsightLoader: React.FC = ({ isLoading, lensResponse }) => { const { http, notifications, inference } = useKibana().services; const [insightResult, setInsightResult] = useState(''); const { defaultConnectorId } = useDefaultAIConnectorId(); + const exportContext = useAIValueExportContext(); + const setInsightForExportContext = exportContext?.setInsight; - const hasEnterpriseLicence = licenseService.isEnterprise(); + const hasEnterpriseLicense = licenseService.isEnterprise(); const { hasAssistantPrivilege, isAssistantEnabled } = useAssistantAvailability(); const prompts = useFindCostSavingsPrompts({ context: { isAssistantEnabled: - hasEnterpriseLicence && (isAssistantEnabled ?? false) && (hasAssistantPrivilege ?? false), + hasEnterpriseLicense && (isAssistantEnabled ?? false) && (hasAssistantPrivilege ?? false), httpFetch: http.fetch, toasts: notifications.toasts, }, @@ -73,6 +72,22 @@ export const CostSavingsKeyInsight: React.FC = ({ isLoading, lensResponse useEffect(() => { if (!lensResponse) setInsightResult(''); }, [lensResponse]); + useEffect( + () => setInsightForExportContext?.(insightResult), + [setInsightForExportContext, insightResult] + ); + return ; +}; + +interface ViewProps { + insight: string; + isLoading: boolean; +} + +const CostSavingsKeyInsightView: React.FC = ({ insight, isLoading }) => { + const { + euiTheme: { size }, + } = useEuiTheme(); return (
= ({ isLoading, lensResponse - {insightResult && !isLoading ? ( - + {insight && !isLoading ? ( + ) : ( )} @@ -116,6 +131,32 @@ export const CostSavingsKeyInsight: React.FC = ({ isLoading, lensResponse ); }; +export const CostSavingsKeyInsight: React.FC = (props) => { + const exportContext = useAIValueExportContext(); + const Loading = ; + + if (props.isLoading) { + return Loading; + } + + if (exportContext?.forwardedState?.insight) { + const { + forwardedState: { insight }, + isInsightVerified, + shouldRegenerateInsight, + } = exportContext; + if (!isInsightVerified) { + return Loading; + } + + if (shouldRegenerateInsight === false) { + return ; + } + } + + return ; +}; + const getPrompt = (result: string, prompts: { part1: string; part2: string }) => { const prompt = `${prompts.part1} diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx index 18959399f6528..ab6cd40c552c7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx @@ -10,6 +10,8 @@ import { render } from '@testing-library/react'; import { CostSavingsMetric } from './cost_savings_metric'; import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable'; import { useSignalIndexWithDefault } from '../../hooks/use_signal_index_with_default'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; +import { useMetricAnimation } from '../../hooks/use_metric_animation'; // Mock VisualizationEmbeddable jest.mock('../../../common/components/visualization_actions/visualization_embeddable', () => ({ @@ -20,6 +22,17 @@ jest.mock('../../hooks/use_signal_index_with_default', () => ({ useSignalIndexWithDefault: jest.fn(), })); +jest.mock('../../providers/ai_value/export_provider', () => ({ + useAIValueExportContext: jest.fn(), +})); + +jest.mock('../../hooks/use_metric_animation', () => ({ + useMetricAnimation: jest.fn(), +})); + +const useAIValueExportContextMock = useAIValueExportContext as jest.Mock; +const useMetricAnimationMock = useMetricAnimation as jest.Mock; + const defaultProps = { from: '2023-01-01T00:00:00.000Z', to: '2023-01-31T23:59:59.999Z', @@ -56,4 +69,28 @@ describe('CostSavingsMetric', () => { render(); expect(mockUseSignalIndexWithDefault).toHaveBeenCalled(); }); + + it('calls useAIValueExportContext hook', () => { + render(); + expect(useAIValueExportContextMock).toHaveBeenCalled(); + }); + + it('calls useMetricAnimationMock hook', () => { + render(); + expect(useMetricAnimationMock).toHaveBeenCalled(); + }); + + describe('export mode', () => { + beforeEach(() => { + // Force it into export mode + useAIValueExportContextMock.mockReturnValue({ + forwardedState: {}, + }); + }); + + it('should not attempt to animate the component', () => { + render(); + expect(useMetricAnimationMock).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx index 42a9c4cf48b8b..5cb8a455ad862 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx @@ -19,6 +19,7 @@ import { VisualizationEmbeddable } from '../../../common/components/visualizatio import { getCostSavingsMetricLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/ai/cost_savings_metric'; import { useMetricAnimation } from '../../hooks/use_metric_animation'; import { useSignalIndexWithDefault } from '../../hooks/use_signal_index_with_default'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { from: string; @@ -28,6 +29,16 @@ interface Props { } const ID = 'CostSavingsMetricQuery'; +const WithMetricAnimation = ({ children }: { children: React.ReactNode }) => { + // Apply animation to the metric value + useMetricAnimation({ + animationDurationMs: 1500, + selector: '.echMetricText__value', + }); + + return <>{children}; +}; + /** * Renders a Lens embeddable metric visualization showing estimated cost savings * based on the number of AI filtered alerts, minutes saved per alert, @@ -44,11 +55,9 @@ const CostSavingsMetricComponent: React.FC = ({ euiTheme: { colors }, } = useEuiTheme(); - // Apply animation to the metric value - useMetricAnimation({ - animationDurationMs: 1500, - selector: '.echMetricText__value', - }); + const exportContext = useAIValueExportContext(); + const isExportMode = exportContext?.forwardedState !== undefined; + const signalIndexName = useSignalIndexWithDefault(); const timerange = useMemo(() => ({ from, to }), [from, to]); const getLensAttributes = useCallback( @@ -63,7 +72,7 @@ const CostSavingsMetricComponent: React.FC = ({ [analystHourlyRate, colors.backgroundBaseSuccess, minutesPerAlert, signalIndexName] ); - return ( + const Visualization = (
= ({ />
); + + if (isExportMode) { + return Visualization; + } + + return {Visualization}; }; export const CostSavingsMetric = React.memo(CostSavingsMetricComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.test.tsx index 4654eab09ed11..f8c89ba61a4b4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.test.tsx @@ -19,6 +19,7 @@ import { AlertProcessing } from './alert_processing'; import { CostSavingsTrend } from './cost_savings_trend'; import { ValueReportSettings } from './value_report_settings'; import type { StartServices } from '../../../types'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; // Mock dependencies jest.mock('../../../common/lib/kibana', () => ({ @@ -45,8 +46,13 @@ jest.mock('./value_report_settings', () => ({ ValueReportSettings: jest.fn(() =>
), })); +jest.mock('../../providers/ai_value/export_provider', () => ({ + useAIValueExportContext: jest.fn(), +})); + const mockUseKibana = useKibana as jest.Mock; const mockUseValueMetrics = useValueMetrics as jest.MockedFunction; +const useAIValueExportContextMock = useAIValueExportContext as jest.Mock; const defaultProps = { setHasAttackDiscoveries: jest.fn(), @@ -91,6 +97,7 @@ describe('AIValueMetrics', () => { beforeEach(() => { jest.clearAllMocks(); + useAIValueExportContextMock.mockReturnValue(undefined); mockUseKibana.mockReturnValue(createMockKibanaServices()); mockUseValueMetrics.mockReturnValue({ @@ -196,6 +203,43 @@ describe('AIValueMetrics', () => { }); }); + it('uses the specified timerange when exporting the report', () => { + const timeRange = { + to: '2025-11-18T13:18:59.691Z', + from: '2025-10-18T12:18:59.691Z', + }; + useAIValueExportContextMock.mockReturnValue({ + forwardedState: { + timeRange, + }, + }); + + render(); + + expect(mockUseValueMetrics).toHaveBeenCalledWith( + expect.objectContaining({ + ...timeRange, + }) + ); + }); + + it('should set the report input in the export context when the data is loaded', () => { + const setReportInputMock = jest.fn(); + useAIValueExportContextMock.mockReturnValue({ + setReportInput: setReportInputMock, + }); + + render(); + + expect(setReportInputMock).toHaveBeenCalledWith({ + attackAlertIds: ['alert-1', 'alert-2'], + analystHourlyRate: 50, + minutesPerAlert: 10, + valueMetrics: mockValueMetrics, + valueMetricsCompare: mockValueMetricsCompare, + }); + }); + it('handles different uiSettings values correctly', () => { mockUseKibana.mockReturnValue( createMockKibanaServices({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx index 38c1eda95c29d..ff289889fe0e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx @@ -18,6 +18,7 @@ import { ExecutiveSummary } from './executive_summary'; import { AlertProcessing } from './alert_processing'; import { useValueMetrics } from '../../hooks/use_value_metrics'; import { useKibana } from '../../../common/lib/kibana'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { setHasAttackDiscoveries: React.Dispatch; @@ -25,8 +26,20 @@ interface Props { to: string; } -export const AIValueMetrics: React.FC = ({ setHasAttackDiscoveries, from, to }) => { +export const AIValueMetrics: React.FC = (props) => { + const { setHasAttackDiscoveries } = props; const { uiSettings } = useKibana().services; + const exportContext = useAIValueExportContext(); + const setReportInputForExportContext = exportContext?.setReportInput; + + let from = props.from; + let to = props.to; + + if (exportContext?.forwardedState) { + const { timeRange } = exportContext.forwardedState; + from = timeRange.from; + to = timeRange.to; + } const { analystHourlyRate, minutesPerAlert } = useMemo( () => ({ @@ -35,9 +48,7 @@ export const AIValueMetrics: React.FC = ({ setHasAttackDiscoveries, from, }), [uiSettings] ); - const { - euiTheme: { colors }, - } = useEuiTheme(); + const { attackAlertIds, isLoading, valueMetrics, valueMetricsCompare } = useValueMetrics({ from, to, @@ -50,10 +61,35 @@ export const AIValueMetrics: React.FC = ({ setHasAttackDiscoveries, from, [valueMetrics.attackDiscoveryCount] ); + useEffect(() => { + if (isLoading || !setReportInputForExportContext) { + return; + } + setReportInputForExportContext({ + attackAlertIds, + valueMetrics, + valueMetricsCompare, + analystHourlyRate, + minutesPerAlert, + }); + }, [ + isLoading, + attackAlertIds, + valueMetrics, + valueMetricsCompare, + analystHourlyRate, + minutesPerAlert, + setReportInputForExportContext, + ]); + useEffect(() => { setHasAttackDiscoveries(hasAttackDiscoveries); }, [hasAttackDiscoveries, setHasAttackDiscoveries]); + const { + euiTheme: { colors }, + } = useEuiTheme(); + return (
({ useKibana: jest.fn() })); +const useKibanaMock = useKibana as jest.Mock; + +jest.mock('../providers/ai_value/export_provider', () => ({ useAIValueExportContext: jest.fn() })); +const useAIValueExportContextMock = useAIValueExportContext as jest.Mock; + +const shareServiceMock = { + toggleShareContextMenu: jest.fn(), +}; + +const buildForwardedStateMock = jest.fn(); + +const anchorElementMock = { someAnchorElementProp: 'baz' } as unknown as HTMLElement; + +const timeRange = { + to: '2025-11-18T13:18:59.691Z', + from: '2025-10-18T12:18:59.691Z', +}; + +const mockKibana = (share: typeof shareServiceMock | undefined, serverless: boolean) => + useKibanaMock.mockReturnValue({ + services: { + share, + serverless, + }, + }); + +type HookResult = ReturnType; +const TestComponent = ({ + anchorElement, + hookValueFn, +}: { + anchorElement: HTMLElement | null; + hookValueFn: (value: HookResult) => void; +}) => { + const hookValue = useDownloadAIValueReport({ + anchorElement, + timeRange, + }); + + hookValueFn(hookValue); + return <>; +}; + +describe('useDownloadAIValueReport', () => { + beforeEach(() => { + // We set all the conditions so that the report is enabled. + // Then we toggle each condition off in the subsequent describe statements as needed + mockKibana(shareServiceMock, false); + + useAIValueExportContextMock.mockReturnValue({ + buildForwardedState: buildForwardedStateMock, + }); + }); + let hookResult: HookResult = { isExportEnabled: true, toggleContextMenu: noop }; + const callHook = (anchorElement: HTMLElement | null) => { + render( + { + hookResult = value; + }} + /> + ); + }; + describe('when it is used in serverless', () => { + beforeEach(() => { + mockKibana(shareServiceMock, true); + callHook(anchorElementMock); + }); + it('should set isExportEnabled to false', () => { + expect(hookResult.isExportEnabled).toBe(false); + }); + + it('should not call shareService.toggleShareContextMenu', () => { + hookResult.toggleContextMenu(); + expect(shareServiceMock.toggleShareContextMenu).not.toHaveBeenCalled(); + }); + }); + + describe('when the anchor element is null', () => { + beforeEach(() => { + callHook(null); + }); + it('should set isExportEnabled to false', () => { + expect(hookResult.isExportEnabled).toBe(false); + }); + + it('should not call shareService.toggleShareContextMenu', () => { + hookResult.toggleContextMenu(); + expect(shareServiceMock.toggleShareContextMenu).not.toHaveBeenCalled(); + }); + }); + + describe('when the there is not a forwardedState', () => { + beforeEach(() => { + useAIValueExportContextMock.mockResolvedValue({}); + callHook(anchorElementMock); + }); + + it('should set isExportEnabled to false', () => { + expect(hookResult.isExportEnabled).toBe(false); + }); + it('should not call shareService.toggleShareContextMenu', () => { + hookResult.toggleContextMenu(); + expect(shareServiceMock.toggleShareContextMenu).not.toHaveBeenCalled(); + }); + }); + + describe('when the shareService is not available', () => { + beforeEach(() => { + mockKibana(undefined, false); + callHook(anchorElementMock); + }); + it('should set isExportEnabled to false', () => { + expect(hookResult.isExportEnabled).toBe(false); + }); + }); + + describe('when the export is enabled', () => { + const forwardedState = { timeRange }; + beforeEach(() => { + buildForwardedStateMock.mockReturnValue(forwardedState); + callHook(anchorElementMock); + }); + it('should set isExportEnabled to true', () => { + expect(hookResult.isExportEnabled).toBe(true); + }); + + it('should call shareService.toggleShareContextMenu with the right parameters', () => { + hookResult.toggleContextMenu(); + expect(shareServiceMock.toggleShareContextMenu).toHaveBeenCalledWith( + expect.objectContaining({ + allowShortUrl: false, + anchorElement: anchorElementMock, + asExport: true, + isDirty: false, + objectType: 'ai_value_report', + objectTypeMeta: { + config: { + integration: { + export: { + pdfReports: {}, + }, + }, + }, + title: 'Download this report', + }, + sharingData: { + locatorParams: { + id: 'AI_VALUE_REPORT_LOCATOR', + params: forwardedState, + }, + title: 'AI Value Report', + }, + }) + ); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx new file mode 100644 index 0000000000000..84d2e38db45de --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { AI_VALUE_REPORT_LOCATOR } from '@kbn/deeplinks-analytics'; +import { useMemo } from 'react'; +import { useKibana } from '../../common/lib/kibana'; +import type { AIValueReportParams } from '../../../common/locators/ai_value_report/locator'; +import { useAIValueExportContext } from '../providers/ai_value/export_provider'; + +interface UseDownloadAIValueReportParams { + anchorElement: HTMLElement | null; + timeRange: AIValueReportParams['timeRange']; +} + +export const useDownloadAIValueReport = ({ + anchorElement, + timeRange, +}: UseDownloadAIValueReportParams) => { + const { share: shareService, serverless } = useKibana().services; + const isServerless = !!serverless; + const aiValueExportContext = useAIValueExportContext(); + + const forwardedState = useMemo(() => { + if (!aiValueExportContext?.buildForwardedState) { + return undefined; + } + + return aiValueExportContext.buildForwardedState({ timeRange }); + }, [timeRange, aiValueExportContext]); + + const isExportEnabled = + forwardedState !== undefined && + // exporting the report via the share service is only available in ESS + !isServerless && + anchorElement !== null && + shareService !== undefined; + + const toggleContextMenu = useMemo(() => { + if (!isExportEnabled) { + return () => {}; + } + + return () => { + shareService.toggleShareContextMenu({ + isDirty: false, + anchorElement, + allowShortUrl: false, + asExport: true, + objectType: 'ai_value_report', + objectTypeMeta: { + title: i18n.translate('xpack.securitySolution.reports.aiValue.shareModal.title', { + defaultMessage: 'Download this report', + }), + config: { + integration: { + export: { + pdfReports: {}, + }, + }, + }, + }, + sharingData: { + title: i18n.translate('xpack.securitySolution.reports.aiValue.pdfReportJobTitle', { + // TODO confirm what wording we want hre + defaultMessage: 'AI Value Report', + }), + locatorParams: { + id: AI_VALUE_REPORT_LOCATOR, + params: forwardedState, + }, + }, + }); + }; + }, [anchorElement, shareService, forwardedState, isExportEnabled]); + + return { + toggleContextMenu, + isExportEnabled, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx index 47ef21c4a6ff4..584a3f5f68afb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useMemo } from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import type { DocLinks } from '@kbn/doc-links'; import { pick } from 'lodash/fp'; @@ -26,6 +26,12 @@ import { useDataView } from '../../data_view_manager/hooks/use_data_view'; import { PageLoader } from '../../common/components/page_loader'; import { inputsSelectors } from '../../common/store'; import { useHasSecurityCapability } from '../../helper_hooks'; +import { useKibana } from '../../common/lib/kibana'; +import { useDownloadAIValueReport } from '../hooks/use_download_ai_value_report'; +import { + AIValueExportProvider, + useAIValueExportContext, +} from '../providers/ai_value/export_provider'; /** * The dashboard includes key performance metrics such as: @@ -41,7 +47,9 @@ import { useHasSecurityCapability } from '../../helper_hooks'; * Data sources and calculation methods are transparent and documented for auditability. */ -const AIValueComponent = () => { +const BaseComponent = () => { + const exportContext = useAIValueExportContext(); + const isExportMode = exportContext?.forwardedState !== undefined; const { loading: oldIsSourcererLoading } = useSourcererDataView(); const { from, to } = useDeepEqualSelector((state) => pick(['from', 'to'], inputsSelectors.valueReportTimeRangeSelector(state)) @@ -57,9 +65,49 @@ const AIValueComponent = () => { const [hasAttackDiscoveries, setHasAttackDiscoveries] = useState(false); const exportPDFRef = useRef<(() => void) | null>(null); + const { serverless } = useKibana().services; + const isServerless = !!serverless; + + const exportButtonRef = useRef(null); + // since we do not have a search bar in the AI Value page, we need to sync the timerange useSyncTimerangeUrlParam(); + const timeRange = useMemo(() => ({ to, from }), [to, from]); + + const { toggleContextMenu, isExportEnabled } = useDownloadAIValueReport({ + anchorElement: exportButtonRef.current, + timeRange, + }); + + const exportButton = useMemo( + () => + isServerless ? ( + exportPDFRef.current?.()} + size="s" + aria-label={EXPORT_REPORT} + > + {EXPORT_REPORT} + + ) : ( + + {EXPORT_REPORT} + + ), + [isServerless, isExportEnabled, exportButtonRef, toggleContextMenu] + ); + if (!hasSocManagementCapability) { return docLinks.siem.privileges} />; } @@ -75,30 +123,22 @@ const AIValueComponent = () => { max-width: 1440px; margin: 0 auto; `} + data-shared-items-container > - , - ...(hasAttackDiscoveries - ? [ - exportPDFRef.current?.()} - size="s" - > - {EXPORT_REPORT} - , - ] - : []), - ]} - /> + {!isExportMode && ( + , + ...(hasAttackDiscoveries ? [exportButton] : []), + ]} + /> + )} {isSourcererLoading ? ( ) : ( @@ -125,4 +165,10 @@ const AIValueComponent = () => { ); }; +const AIValueComponent = () => ( + + + +); + export const AIValue = React.memo(AIValueComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx new file mode 100644 index 0000000000000..2034ab185588c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx @@ -0,0 +1,172 @@ +/* + * 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 { act, render, waitFor, screen } from '@testing-library/react'; +import { useHistory } from 'react-router-dom'; +// Jest's global object does not have the crypto library. +// Therefore we import Node's. +// eslint-disable-next-line import/no-nodejs-modules +import { webcrypto } from 'crypto'; +import { AIValueExportProvider, useAIValueExportContext } from './export_provider'; + +jest.mock('react-router', () => { + return { + useHistory: jest.fn(), + }; +}); +const useHistoryMock = useHistory as jest.Mock; +type ContextValue = ReturnType; + +const reportInput = { + attackAlertIds: ['0f6ca8ce-4ed5-4d71-88a6-fba3f87003f3'], + valueMetrics: { + attackDiscoveryCount: 14, + filteredAlerts: 4952, + filteredAlertsPerc: 99.73816717019133, + escalatedAlertsPerc: 0.2618328298086606, + hoursSaved: 662, + totalAlerts: 4965, + costSavings: 49650, + }, + valueMetricsCompare: { + attackDiscoveryCount: 0, + filteredAlerts: 5035, + filteredAlertsPerc: 100, + escalatedAlertsPerc: 0, + hoursSaved: 671.3333333333334, + totalAlerts: 5035, + costSavings: 50350, + }, + analystHourlyRate: 75, + minutesPerAlert: 8, +}; + +const reportDataHash = '856cbc3cfa41e8458e99f1b017bde73738a10d0cbbe3dea13a4960297957c645'; + +const timeRange = { + to: '2025-11-18T13:18:59.691Z', + from: '2025-10-18T12:18:59.691Z', +}; + +const TestComponent = ({ contextValueFn }: { contextValueFn: (context: ContextValue) => void }) => { + const context = useAIValueExportContext(); + contextValueFn(context); + return ( + <> + {context?.isInsightVerified ? 'Insight verified' : ''} + + {context?.buildForwardedState({ timeRange }) ? 'buildForwardedState available' : ''} + + + ); +}; + +describe('AIValueExportContext', () => { + let context: ContextValue = null; + const doRender = async (locationState: unknown) => { + useHistoryMock.mockReturnValue({ + location: { + state: locationState, + }, + }); + render( + + { + context = contextValue; + }} + /> + + ); + }; + + const verifyInsight = () => waitFor(() => screen.getByText('Insight verified')); + const verifyBuildForwardedStateFnAvailable = () => + waitFor(() => screen.getByText('buildForwardedState available')); + + const setReportInput = (input: typeof reportInput) => act(() => context?.setReportInput(input)); + const setInsight = (insight: string) => act(() => context?.setInsight(insight)); + + beforeEach(() => { + Object.defineProperties(global, { + crypto: { value: webcrypto, writable: true }, + }); + }); + + const forwardedState = { + timeRange, + insight: 'Some valuable insight', + reportDataHash, + }; + + describe('export mode: the page is being rendered in the backend for export', () => { + describe('when there is a forwarded state is valid', () => { + beforeEach(async () => { + doRender(forwardedState); + setReportInput(reportInput); + await verifyInsight(); + }); + it('should parse the forwarded state correctly', () => { + expect(context?.forwardedState).toEqual(forwardedState); + }); + + it('should verify the insight', () => { + expect(context?.isInsightVerified).toBe(true); + }); + + it('should indicate that the insight should NOT be regenerated', () => { + expect(context?.shouldRegenerateInsight).toBe(false); + }); + }); + + describe('when there is a forwarded state is valid and the report input is different', () => { + beforeEach(async () => { + doRender(forwardedState); + setReportInput({ ...reportInput, minutesPerAlert: 12345 }); + await verifyInsight(); + }); + it('should verify the insight', () => { + expect(context?.isInsightVerified).toBe(true); + }); + + it('should indicate that the insight should be regenerated', () => { + expect(context?.shouldRegenerateInsight).toBe(true); + }); + }); + + describe('when the forwarded state is invalid', () => { + beforeEach(() => { + doRender('something unexpected'); + }); + it('should set the forwarded state to undefined', () => { + expect(context?.forwardedState).toBe(undefined); + }); + }); + }); + + describe("normal mode: the page is rendered in the user's browser", () => { + beforeEach(() => { + doRender(undefined); + }); + + it('should expose a buildForwardedState that is defined only when the insight and the report input has loaded', async () => { + const buildState = () => context?.buildForwardedState({ timeRange }); + expect(buildState()).toBeUndefined(); + setInsight('Some valuable insight'); + expect(buildState()).toBeUndefined(); + setReportInput(reportInput); + await verifyBuildForwardedStateFnAvailable(); + + expect(buildState()).toEqual({ + timeRange, + insight: 'Some valuable insight', + reportDataHash, + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx new file mode 100644 index 0000000000000..82c0a321adc38 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx @@ -0,0 +1,125 @@ +/* + * 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, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import type { ForwardedAIValueReportState } from '../../../../common/locators/ai_value_report/locator'; +import { parseLocationState } from '../../../../common/locators/ai_value_report/locator'; + +interface AIValueExportContext { + forwardedState?: ForwardedAIValueReportState; + isInsightVerified: boolean; + shouldRegenerateInsight?: boolean; + setReportInput: (inputData: object) => void; + setInsight: (insight: string) => void; + buildForwardedState: ( + params: Pick + ) => ForwardedAIValueReportState | undefined; +} + +const AIValueExportContext = createContext(null); + +export const useAIValueExportContext = () => useContext(AIValueExportContext); + +interface AIValueExportProviderProps { + children: React.ReactNode; +} + +const hashReportData = async (data: object) => { + const str = JSON.stringify(data); + const enc = new TextEncoder(); + const hash = await crypto.subtle.digest('SHA-256', enc.encode(str)); + return Array.from(new Uint8Array(hash)) + .map((v) => v.toString(16).padStart(2, '0')) + .join(''); +}; + +/** + * This provider manages context for the AI Value Report. + * It exposes hooks for setting the report’s input data and the AI-generated + * cost-savings trend insight when the report is loaded. + * + * After these values are set, the `buildForwardedState` function becomes ready + * to use when exporting the report to PDF. Only the AI-generated insight and a + * hash of the input data it was derived from are included in the forwarded + * state. This ensures the backend does not regenerate the insight (and consume + * AI tokens) if the input data has not changed. + * + * If the navigation history contains a state (indicating that the page is being + * exported, such as to PDF), the provider parses that state and extracts the + * AI-generated insight along with the input-data hash. It then waits for the + * report data to load, hashes it, and checks it against the forwarded hash. + * If the hashes match, `shouldRegenerateInsight` is set to false; otherwise it + * is set to true. + */ +export function AIValueExportProvider({ children }: AIValueExportProviderProps) { + const history = useHistory(); + + const [forwardedState, setForwardedState] = useState(); + const [isInsightVerified, setIsInsightVerified] = useState(false); + const [shouldRegenerateInsight, setShouldRegenerateInsight] = useState(); + + const [reportInput, setReportInput] = useState(); + const [reportDataHash, setReportDataHash] = useState(); + + const [insight, setInsight] = useState(); + + useEffect(() => { + if (history.location.state) { + setForwardedState(parseLocationState(history.location.state)); + } + }, [history.location.state]); + + useEffect(() => { + if (reportInput) { + setReportDataHash(undefined); + const generateReportDataHash = async () => { + setReportDataHash(await hashReportData(reportInput)); + }; + + generateReportDataHash(); + } + }, [reportInput]); + + useEffect(() => { + if (forwardedState && reportDataHash) { + setShouldRegenerateInsight(reportDataHash !== forwardedState.reportDataHash); + setIsInsightVerified(true); + } + }, [forwardedState, reportDataHash]); + + const buildForwardedState = useCallback( + ({ + timeRange, + }: Pick): ForwardedAIValueReportState | undefined => { + if (!insight || !reportDataHash) { + return undefined; + } + + return { + timeRange, + insight, + reportDataHash, + }; + }, + [insight, reportDataHash] + ); + + const value = useMemo( + () => ({ + forwardedState, + isInsightVerified, + shouldRegenerateInsight, + buildForwardedState, + setInsight, + setReportInput, + }), + [forwardedState, buildForwardedState, isInsightVerified, shouldRegenerateInsight] + ); + + return {children}; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 5184abab328e7..a628bc698eefb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -146,6 +146,7 @@ import { HealthDiagnosticServiceImpl } from './lib/telemetry/diagnostic/health_d import type { HealthDiagnosticService } from './lib/telemetry/diagnostic/health_diagnostic_service.types'; import { ENTITY_RISK_SCORE_TOOL_ID } from './assistant/tools/entity_risk_score/entity_risk_score'; import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; +import { AIValueReportLocatorDefinition } from '../common/locators/ai_value_report/locator'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -231,6 +232,10 @@ export class Plugin implements ISecuritySolutionPlugin { ): SecuritySolutionPluginSetup { this.logger.debug('plugin setup'); + if (plugins.share) { + plugins.share.url.locators.create(new AIValueReportLocatorDefinition()); + } + const { appClientFactory, productFeaturesService, pluginContext, config, logger } = this; const experimentalFeatures = config.experimentalFeatures; diff --git a/x-pack/solutions/security/plugins/security_solution/server/ui_settings.ts b/x-pack/solutions/security/plugins/security_solution/server/ui_settings.ts index 722fa804504a2..36d65633b55f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/ui_settings.ts @@ -492,6 +492,7 @@ export const initUiSettings = ( schema: schema.boolean(), solutionViews: ['classic', 'security'], }, + ...getDefaultValueReportSettings(), ...(experimentalFeatures.disableESQLRiskScoring ? {} : { diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts index aa345a9c7ddba..4f34cd719526e 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/plugin.ts @@ -14,7 +14,6 @@ import type { } from '@kbn/core/server'; import { SECURITY_PROJECT_SETTINGS } from '@kbn/serverless-security-settings'; -import { getDefaultValueReportSettings } from '@kbn/security-solution-plugin/server/ui_settings'; import { getEnabledProductFeatures } from '../common/pli/pli_features'; import type { ServerlessSecurityConfig } from './config'; @@ -90,11 +89,6 @@ export class SecuritySolutionServerlessPlugin // Setup project uiSettings whitelisting pluginsSetup.serverless.setupProjectSettings(projectSettings); - // Serverless Advanced Settings setup - coreSetup.uiSettings.register({ - ...getDefaultValueReportSettings(), - }); - // Tasks this.cloudSecurityUsageReportingTask = new SecurityUsageReportingTask({ core: coreSetup, From c3639fce64a88e706319fb6c27446e46c0236ea0 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Wed, 19 Nov 2025 14:50:50 +0100 Subject: [PATCH 03/34] tune cost savings insight prompt limit it to 500 words --- .../plugins/elastic_assistant/server/lib/prompt/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts index 2c9191c657cae..6cd8eedb68e02 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts @@ -333,7 +333,7 @@ export const starterPromptPrompt4 = 'Can you provide examples of questions I can ask about Elastic Security, such as investigating alerts, running ES|QL queries, incident response, or threat intelligence?'; export const costSavingsInsightPart1 = `You are given Elasticsearch Lens aggregation results showing cost savings over time:`; -export const costSavingsInsightPart2 = `Generate a concise bulleted summary in mdx markdown. Follow the style and tone of the example below, highlighting key trends, averages, peaks, and projections: +export const costSavingsInsightPart2 = `Generate a concise bulleted summary in mdx markdown, no more than 500 characters. Follow the style and tone of the example below, highlighting key trends, averages, peaks, and projections: \`\`\` - Between July 18 and August 18, daily cost savings **averaged around $135K** From 28fa6ed7dc4270e099b95f199678018db4acc4b5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:09:55 +0000 Subject: [PATCH 04/34] Changes from node scripts/lint_ts_projects --fix --- .../solutions/security/plugins/security_solution/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 8ee1d902e8259..437a076bd9444 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -262,5 +262,6 @@ "@kbn/response-ops-rule-form", "@kbn/core-lifecycle-browser-mocks", "@kbn/connector-schemas", + "@kbn/deeplinks-analytics", ] } From daacc5df3be69fced63e15959fd524a508ce4425 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Wed, 19 Nov 2025 16:16:35 +0100 Subject: [PATCH 05/34] update securitySolution bundle limits --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1fab56104ca16..e2c8670201656 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -154,7 +154,7 @@ pageLoadAssetSize: searchQueryRules: 6689 searchSynonyms: 6371 security: 79627 - securitySolution: 119378 + securitySolution: 186285 securitySolutionEss: 43319 securitySolutionServerless: 62689 serverless: 7852 From 0b39652fe8558b464c19ead2d2dd5de3153f8760 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Thu, 20 Nov 2025 14:52:46 +0100 Subject: [PATCH 06/34] fix unit tests --- .../components/search_bar/index.test.tsx | 4 ++ .../public/reports/pages/ai_value.test.tsx | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.test.tsx index 688d394f7f4b1..6f60e94541726 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/search_bar/index.test.tsx @@ -363,6 +363,10 @@ describe('SearchBarComponent', () => { timerange: mockGlobalState.inputs.timeline.timerange, linkTo: mockGlobalState.inputs.timeline.linkTo, }, + valueReport: { + timerange: mockGlobalState.inputs.valueReport.timerange, + linkTo: mockGlobalState.inputs.valueReport.linkTo, + }, }, ]); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx index 20eff4ffbbbd0..089bf4eaeec1b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx @@ -18,6 +18,7 @@ import { useDataView } from '../../data_view_manager/hooks/use_data_view'; import { useHasSecurityCapability } from '../../helper_hooks'; import { TestProviders } from '../../common/mock/test_providers'; import * as i18n from './translations'; +import { useAIValueExportContext } from '../providers/ai_value/export_provider'; // Mock all dependencies before imports to avoid issues jest.mock('../../common/hooks/search_bar/use_sync_timerange_url_param', () => ({ @@ -44,6 +45,15 @@ jest.mock('../../helper_hooks', () => ({ useHasSecurityCapability: jest.fn(), })); +jest.mock('../providers/ai_value/export_provider', () => { + return { + AIValueExportProvider: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + useAIValueExportContext: jest.fn(), + }; +}); + // Mock docLinks for NoPrivileges component jest.mock('@kbn/doc-links', () => ({ getDocLinksMeta: jest.fn(() => ({})), @@ -127,6 +137,8 @@ const mockUseHasSecurityCapability = useHasSecurityCapability as jest.MockedFunc typeof useHasSecurityCapability >; +const mockUseAIValueExportContext = useAIValueExportContext as jest.Mock; + describe('AIValue', () => { beforeEach(() => { jest.clearAllMocks(); @@ -259,6 +271,16 @@ describe('AIValue', () => { const datePicker = screen.getByTestId('superDatePickerToggleQuickMenuButton'); expect(datePicker).toBeInTheDocument(); }); + + it('should be wrapped in a AIValueExportProvider', () => { + render( + + + + ); + + expect(screen.getByTestId('AIValueExportProvider')).toBeInTheDocument(); + }); }); describe('Hook Integration', () => { @@ -274,6 +296,7 @@ describe('AIValue', () => { expect(mockUseIsExperimentalFeatureEnabled).toHaveBeenCalledWith('newDataViewPickerEnabled'); expect(mockUseHasSecurityCapability).toHaveBeenCalledWith('socManagement'); expect(mockUseAlertsPrivileges).toHaveBeenCalled(); + expect(mockUseAIValueExportContext).toHaveBeenCalled(); }); }); @@ -314,4 +337,27 @@ describe('AIValue', () => { expect(screen.getByTestId('aiValueLoader')).toBeInTheDocument(); }); }); + + describe('export mode', () => { + beforeEach(() => { + mockUseAIValueExportContext.mockReturnValue({ + forwardedState: { + timeRange: { + from: '2025-01-01T00:00:00.000Z', + to: '2025-01-31T23:59:59.999Z', + }, + }, + }); + }); + + it('should not render the header of the page', () => { + render( + + + + ); + + expect(screen.queryByTestId('header-page')).not.toBeInTheDocument(); + }); + }); }); From 2b3fd3d2d51252ee46d62cf30d7f9920fc1b91dc Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Fri, 21 Nov 2025 11:03:25 +0100 Subject: [PATCH 07/34] add new ESS settings to stack management schema --- .../server/collectors/management/schema.ts | 12 ++++++++++++ .../server/collectors/management/types.ts | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts index 1fdd8111410c1..1b4da515511fb 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts @@ -42,6 +42,18 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, }, + 'securitySolution:defaultValueReportMinutes': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, + 'securitySolution:defaultValueReportRate': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, + 'securitySolution:defaultValueReportTitle': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, 'xpackReporting:customPdfLogo': { type: 'keyword', _meta: { description: 'Default value of the setting was changed.' }, diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts index 7b8282986598c..cb36c6ee5effd 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts @@ -78,6 +78,9 @@ export interface UsageStats { 'securitySolution:enableEsqlRiskScoring': boolean; 'securitySolution:enableCloudConnector': boolean; 'securitySolution:suppressionBehaviorOnAlertClosure': string; + 'securitySolution:defaultValueReportMinutes': string; + 'securitySolution:defaultValueReportRate': string; + 'securitySolution:defaultValueReportTitle': string; 'search:includeFrozen': boolean; 'courier:maxConcurrentShardRequests': number; 'courier:setRequestPreference': string; From eeef8ea4041b26155bc335d0b0e81477fdf8962f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:21:43 +0000 Subject: [PATCH 08/34] Changes from node scripts/telemetry_check --- .../shared/telemetry/schema/oss_platform.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/platform/plugins/shared/telemetry/schema/oss_platform.json b/src/platform/plugins/shared/telemetry/schema/oss_platform.json index fce1d8d948005..e06f515e0a954 100644 --- a/src/platform/plugins/shared/telemetry/schema/oss_platform.json +++ b/src/platform/plugins/shared/telemetry/schema/oss_platform.json @@ -10109,6 +10109,24 @@ "description": "Non-default value of setting." } }, + "securitySolution:defaultValueReportMinutes": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, + "securitySolution:defaultValueReportRate": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, + "securitySolution:defaultValueReportTitle": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, "xpackReporting:customPdfLogo": { "type": "keyword", "_meta": { From 8ba9c2d6398dd27279cdecaace8d16d0c59b76dd Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Fri, 21 Nov 2025 17:19:20 +0100 Subject: [PATCH 09/34] fix cypress tests --- .../cypress/e2e/explore/urls/state.cy.ts | 12 ++++++------ .../security_solution_cypress/cypress/urls/state.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/explore/urls/state.cy.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/explore/urls/state.cy.ts index fb5aae03a9c3a..9db51acb111b4 100644 --- a/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/explore/urls/state.cy.ts +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/explore/urls/state.cy.ts @@ -237,7 +237,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => { 'contain', "/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')" + "&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" + - "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" + "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),valueReport:(linkTo:!(),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" ); }); it('sets KQL in host page and detail page and check if href match on breadcrumb, tabs and subTabs', () => { @@ -253,7 +253,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => { 'contain', "/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')" + "&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" + - "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))" + "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),valueReport:(linkTo:!(),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" ); cy.get(NETWORK) .should('have.attr', 'href') @@ -261,7 +261,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => { 'contain', "/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')" + "&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" + - "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))" + "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),valueReport:(linkTo:!(),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" ); toggleNavigationPanel(EXPLORE_PANEL_BTN); cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana'); @@ -275,7 +275,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => { .and( 'contain', "/app/security/hosts/name/siem-kibana/anomalies?timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" + - "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))" + "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),valueReport:(linkTo:!(),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" ); cy.get(BREADCRUMBS) @@ -285,7 +285,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => { 'contain', "/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" + "&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" + - "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))" + "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),valueReport:(linkTo:!(),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" ); cy.get(BREADCRUMBS) .eq(3) @@ -294,7 +294,7 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => { 'contain', "/app/security/hosts/name/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" + "&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" + - "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))" + "&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),valueReport:(linkTo:!(),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))" ); }); diff --git a/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/state.ts b/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/state.ts index 5c4db26382a4c..59e50fb655730 100644 --- a/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/state.ts +++ b/x-pack/solutions/security/test/security_solution_cypress/cypress/urls/state.ts @@ -6,7 +6,7 @@ */ export const ABSOLUTE_DATE_RANGE = { - url: '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', + url: '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),valueReport:(linkTo:!(),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', urlWithTimestamps: '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', @@ -19,7 +19,7 @@ export const ABSOLUTE_DATE_RANGE = { urlHost: '/app/security/hosts/allHosts?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', urlHostNew: - '/app/security/hosts/allHosts?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272023-01-01T21:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272023-01-01T21:33:29.186Z%27)))', + '/app/security/hosts/allHosts?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272023-01-01T21:33:29.186Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272023-01-01T21:33:29.186Z%27)),valueReport:(linkTo:!(),timerange:(from:%272019-08-01T20:03:29.186Z%27,kind:absolute,to:%272019-08-01T20:33:29.186Z%27)))', urlFiltersHostsHosts: '/app/security/hosts/allHosts?filters=!((%27$state%27:(store:globalState),meta:(alias:!n,disabled:!f,key:host.name,negate:!f,params:(query:test-host),type:phrase),query:(match_phrase:(host.name:(query:test-host)))),(%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:host.os.name,negate:!f,params:(query:test-os),type:phrase),query:(match_phrase:(host.os.name:(query:test-os)))))', From 243caef98daacefc01205d5dd490eeb04fc96532 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:41 +0000 Subject: [PATCH 10/34] Changes from node scripts/regenerate_moon_projects.js --update --- x-pack/solutions/security/plugins/security_solution/moon.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/plugins/security_solution/moon.yml b/x-pack/solutions/security/plugins/security_solution/moon.yml index 11099b78e5865..29ad4e26b00e2 100644 --- a/x-pack/solutions/security/plugins/security_solution/moon.yml +++ b/x-pack/solutions/security/plugins/security_solution/moon.yml @@ -261,6 +261,7 @@ dependsOn: - '@kbn/response-ops-rule-form' - '@kbn/core-lifecycle-browser-mocks' - '@kbn/connector-schemas' + - '@kbn/deeplinks-analytics' - '@kbn/tour-queue' tags: - plugin From 400824f82d103454b8d75feff23c4410f21d8645 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 14:56:58 +0100 Subject: [PATCH 11/34] enable scheduling an export report task --- .../scheduled_report_share_integration.test.tsx | 10 ---------- .../scheduled_report_share_integration.tsx | 6 ++---- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx index 68229b681159e..21bacee9c0742 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.test.tsx @@ -113,16 +113,6 @@ describe('createScheduledReportShareIntegration', () => { }) ).toBe(false); }); - - it('should return false for unsupported object types', () => { - expect( - integration.prerequisiteCheck!({ - license: { type: SCHEDULED_REPORT_VALID_LICENSES[0] } as ILicense, - capabilities, - objectType: 'ai_value_report', - }) - ).toBe(false); - }); }); describe('config.shouldRender', () => { diff --git a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx index 3a2c93e02e740..588a8330538e9 100644 --- a/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx +++ b/x-pack/platform/plugins/private/reporting/public/management/integrations/scheduled_report_share_integration.tsx @@ -29,8 +29,6 @@ export interface CreateScheduledReportProviderOptions { services: ReportingPublicPluginStartDependencies; } -const unsupportedObjectTypes = ['ai_value_report']; - export const shouldRegisterScheduledReportShareIntegration = async (http: HttpSetup) => { const { isSufficientlySecure, hasPermanentEncryptionKey } = await queryClient.fetchQuery({ queryKey: getReportingHealthQueryKey(), @@ -77,8 +75,8 @@ export const createScheduledReportShareIntegration = ({ flyoutSizing: { size: 'm', maxWidth: 500 }, }; }, - prerequisiteCheck: ({ license, objectType }) => { - if (!license || !license.type || unsupportedObjectTypes.includes(objectType)) { + prerequisiteCheck: ({ license }) => { + if (!license || !license.type) { return false; } return SCHEDULED_REPORT_VALID_LICENSES.includes(license.type); From 920f4e3d7a1309e56d295d4670f00f4236a24e85 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:02:12 +0100 Subject: [PATCH 12/34] use AI_VALUE_REPORT_LOCATOR from @kbn/deeplinks-analytics --- .../security/plugins/security_solution/common/constants.ts | 1 - .../common/locators/ai_value_report/locator.test.ts | 3 ++- .../common/locators/ai_value_report/locator.ts | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 6b3777959d86a..c7a57adad554f 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -54,7 +54,6 @@ export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const; export const DEFAULT_LISTS_INDEX = '.lists' as const; export const DEFAULT_ITEMS_INDEX = '.items' as const; export const DEFAULT_RISK_SCORE_PAGE_SIZE = 1000 as const; -export const AI_VALUE_REPORT_LOCATOR = 'AI_VALUE_REPORT_LOCATOR' as const; // The DEFAULT_MAX_SIGNALS value exists also in `x-pack/platform/plugins/shared/cases/common/constants.ts` // If either changes, engineer should ensure both values are updated export const DEFAULT_MAX_SIGNALS = 100 as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts index a8cdae3afccd6..a0ba324bc55d6 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.test.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { AI_VALUE_REPORT_LOCATOR } from '@kbn/deeplinks-analytics'; import { AIValueReportLocatorDefinition, parseLocationState } from './locator'; -import { AI_VALUE_REPORT_LOCATOR, AI_VALUE_PATH, APP_UI_ID } from '../../constants'; +import { AI_VALUE_PATH, APP_UI_ID } from '../../constants'; describe('AIValueReportLocatorDefinition', () => { const locator = new AIValueReportLocatorDefinition(); diff --git a/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts index 2bc5ee70ed9d4..7b46dc3802a19 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/locators/ai_value_report/locator.ts @@ -5,9 +5,10 @@ * 2.0. */ -import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; import { z } from '@kbn/zod'; -import { AI_VALUE_REPORT_LOCATOR, AI_VALUE_PATH, APP_UI_ID } from '../../constants'; +import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; +import { AI_VALUE_REPORT_LOCATOR } from '@kbn/deeplinks-analytics'; +import { AI_VALUE_PATH, APP_UI_ID } from '../../constants'; const AIValueReportParamsSchema = z.object({ timeRange: z.object({ From 3e9f5eb297b90b89cef7c2f993c4b05240e1d303 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:05:02 +0100 Subject: [PATCH 13/34] Update x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx Co-authored-by: Steph Milovic --- .../reports/components/ai_value/cost_savings_key_insight.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx index c1d16b4df71c5..6c317016ba1fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx @@ -73,7 +73,7 @@ const CostSavingsKeyInsightLoader: React.FC = ({ isLoading, lensResponse if (!lensResponse) setInsightResult(''); }, [lensResponse]); useEffect( - () => setInsightForExportContext?.(insightResult), + () => if (insightResult.length) setInsightForExportContext?.(insightResult), [setInsightForExportContext, insightResult] ); return ; From d0204d256b49de79572c570140eb7967df4a98f5 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:12:48 +0100 Subject: [PATCH 14/34] Update x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx Co-authored-by: Steph Milovic --- .../reports/components/ai_value/index.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx index ff289889fe0e9..2c6f23604ee8e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/index.tsx @@ -32,14 +32,19 @@ export const AIValueMetrics: React.FC = (props) => { const exportContext = useAIValueExportContext(); const setReportInputForExportContext = exportContext?.setReportInput; - let from = props.from; - let to = props.to; - - if (exportContext?.forwardedState) { - const { timeRange } = exportContext.forwardedState; - from = timeRange.from; - to = timeRange.to; - } + const { from, to } = useMemo(() => { + if (exportContext?.forwardedState) { + const { timeRange } = exportContext.forwardedState; + return { + from: timeRange.from, + to: timeRange.to, + }; + } + return { + from: props.from, + to: props.to, + }; + }, [props.from, props.to, exportContext?.forwardedState]); const { analystHourlyRate, minutesPerAlert } = useMemo( () => ({ From 9590b4be55327f7d4194dea9952139824d508046 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:15:24 +0100 Subject: [PATCH 15/34] Update x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx Co-authored-by: Steph Milovic --- .../public/reports/hooks/use_download_ai_value_report.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx index 84d2e38db45de..4908ff41967d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx @@ -31,7 +31,7 @@ export const useDownloadAIValueReport = ({ } return aiValueExportContext.buildForwardedState({ timeRange }); - }, [timeRange, aiValueExportContext]); + }, [timeRange, aiValueExportContext?.buildForwardedState]); const isExportEnabled = forwardedState !== undefined && From 9a47f65be58068f27346793ac9029b20df3d863f Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:18:39 +0100 Subject: [PATCH 16/34] fix syntax error --- .../components/ai_value/cost_savings_key_insight.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx index 6c317016ba1fe..590fb68829944 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx @@ -72,10 +72,11 @@ const CostSavingsKeyInsightLoader: React.FC = ({ isLoading, lensResponse useEffect(() => { if (!lensResponse) setInsightResult(''); }, [lensResponse]); - useEffect( - () => if (insightResult.length) setInsightForExportContext?.(insightResult), - [setInsightForExportContext, insightResult] - ); + useEffect(() => { + if (insightResult.length) { + setInsightForExportContext?.(insightResult); + } + }, [setInsightForExportContext, insightResult]); return ; }; From 45df8ae2673e67d0908851d0ba16ed197839b8d5 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:41:34 +0100 Subject: [PATCH 17/34] use callback ref in ai value page --- .../public/reports/pages/ai_value.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx index 584a3f5f68afb..d4081107d6599 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx @@ -68,7 +68,9 @@ const BaseComponent = () => { const { serverless } = useKibana().services; const isServerless = !!serverless; - const exportButtonRef = useRef(null); + const [exportButtonElement, setExportButtonElement] = useState< + HTMLAnchorElement | HTMLButtonElement | null + >(null); // since we do not have a search bar in the AI Value page, we need to sync the timerange useSyncTimerangeUrlParam(); @@ -76,7 +78,7 @@ const BaseComponent = () => { const timeRange = useMemo(() => ({ to, from }), [to, from]); const { toggleContextMenu, isExportEnabled } = useDownloadAIValueReport({ - anchorElement: exportButtonRef.current, + anchorElement: exportButtonElement, timeRange, }); @@ -96,7 +98,7 @@ const BaseComponent = () => { { {EXPORT_REPORT} ), - [isServerless, isExportEnabled, exportButtonRef, toggleContextMenu] + [isServerless, isExportEnabled, toggleContextMenu] ); if (!hasSocManagementCapability) { From 2789336102eb06db58270cdb6c8c0143b060b493 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Mon, 24 Nov 2025 15:44:48 +0100 Subject: [PATCH 18/34] memoise loading state of CostSavingsKeyInsightView --- .../reports/components/ai_value/cost_savings_key_insight.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx index 590fb68829944..e7fda4caaab10 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -134,7 +134,7 @@ const CostSavingsKeyInsightView: React.FC = ({ insight, isLoading }) export const CostSavingsKeyInsight: React.FC = (props) => { const exportContext = useAIValueExportContext(); - const Loading = ; + const Loading = useMemo(() => , []); if (props.isLoading) { return Loading; From 5c221c0b1d1085a3795c98affdb566e747c2bfc9 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 11:04:38 +0100 Subject: [PATCH 19/34] add telemetry support --- .../telemetry/events/ai_value_report/index.ts | 54 +++++++++++++++++++ .../telemetry/events/ai_value_report/types.ts | 33 ++++++++++++ .../lib/telemetry/events/telemetry_events.ts | 2 + .../public/common/lib/telemetry/types.ts | 10 +++- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/types.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/index.ts new file mode 100644 index 0000000000000..0e53804fecd23 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { AIValueReportTelemetryEvent } from './types'; +import { AIValueReportEventTypes } from './types'; + +export const AIValueReportExportExecutionEvent: AIValueReportTelemetryEvent = { + eventType: AIValueReportEventTypes.AIValueReportExportExecution, + schema: {}, +}; + +export const aiValueReportExportErrorEvent: AIValueReportTelemetryEvent = { + eventType: AIValueReportEventTypes.AIValueReportExportError, + schema: { + errorMessage: { + type: 'text', + _meta: { + description: 'The error message that occurs while exporting the AI Value Report', + optional: false, + }, + }, + isExportMode: { + type: 'boolean', + _meta: { + description: 'Flag indicating if the error occurs in export mode', + optional: false, + }, + }, + }, +}; + +export const aiValueReportExportInsightVerifiedEvent: AIValueReportTelemetryEvent = { + eventType: AIValueReportEventTypes.AIValueReportExportInsightVerified, + schema: { + shouldRegenerate: { + type: 'boolean', + _meta: { + description: + 'Flag indicating if the insight received as parameter in the export should be regenerated', + optional: false, + }, + }, + }, +}; + +export const aiValueReportTelemetryEvents = [ + aiValueReportExportErrorEvent, + AIValueReportExportExecutionEvent, + aiValueReportExportInsightVerifiedEvent, +]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/types.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/types.ts new file mode 100644 index 0000000000000..2c48bef4a3407 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/ai_value_report/types.ts @@ -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 { RootSchema } from '@kbn/core/public'; + +export enum AIValueReportEventTypes { + AIValueReportExportExecution = 'AI Value Report Export Execution', + AIValueReportExportError = 'AI Value Report Export Error', + AIValueReportExportInsightVerified = 'AI Value Report Export Insight Regenerated', +} + +interface ReportAIValueReportExportErrorParams { + errorMessage: string; + isExportMode: boolean; +} + +interface ReportAIValueReportExportInsightVerifiedParams { + shouldRegenerate: boolean; +} + +export interface AIValueReportTelemetryEventsMap { + [AIValueReportEventTypes.AIValueReportExportExecution]: {}; + [AIValueReportEventTypes.AIValueReportExportError]: ReportAIValueReportExportErrorParams; + [AIValueReportEventTypes.AIValueReportExportInsightVerified]: ReportAIValueReportExportInsightVerifiedParams; +} + +export interface AIValueReportTelemetryEvent { + eventType: AIValueReportEventTypes; + schema: RootSchema; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts index a8ff691da9dbc..c1a4eaa790a21 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts @@ -17,6 +17,7 @@ import { onboardingHubTelemetryEvents } from './onboarding'; import { previewRuleTelemetryEvents } from './preview_rule'; import { siemMigrationsTelemetryEvents } from './siem_migrations'; import { ruleUpgradeTelemetryEvents } from './rule_upgrade'; +import { aiValueReportTelemetryEvents } from './ai_value_report'; export const telemetryEvents = [ ...alertsTelemetryEvents, @@ -32,4 +33,5 @@ export const telemetryEvents = [ ...notesTelemetryEvents, ...appTelemetryEvents, ...siemMigrationsTelemetryEvents, + ...aiValueReportTelemetryEvents, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts index 8710a50c596cb..1be2726bedac3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts @@ -51,6 +51,11 @@ import type { RuleUpgradeTelemetryEventsMap, } from './events/rule_upgrade/types'; +import { + AIValueReportEventTypes, + AIValueReportTelemetryEventsMap +} from './events/ai_value_report/types' + export * from './events/app/types'; export * from './events/alerts_grouping/types'; export * from './events/data_quality/types'; @@ -95,6 +100,8 @@ export type TelemetryEventTypeData = T extends Al ? SiemMigrationsTelemetryEventsMap[T] : T extends RuleUpgradeEventTypes ? RuleUpgradeTelemetryEventsMap[T] + : T extends AIValueReportEventTypes + ? AIValueReportTelemetryEventsMap[T] : never; export type TelemetryEventTypes = @@ -111,4 +118,5 @@ export type TelemetryEventTypes = | AppEventTypes | SiemMigrationsRuleEventTypes | SiemMigrationsDashboardEventTypes - | RuleUpgradeEventTypes; + | RuleUpgradeEventTypes + | AIValueReportEventTypes; From cf7bee787adefcc251f50bf2f6064b723a0f71c0 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 12:50:46 +0100 Subject: [PATCH 20/34] added telemetry and error handling to hash generation --- .../ai_value/export_provider.test.tsx | 146 ++++++++++++++++-- .../providers/ai_value/export_provider.tsx | 59 ++++++- 2 files changed, 189 insertions(+), 16 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx index 2034ab185588c..9b1a9dcbac6ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx @@ -13,13 +13,24 @@ import { useHistory } from 'react-router-dom'; // eslint-disable-next-line import/no-nodejs-modules import { webcrypto } from 'crypto'; import { AIValueExportProvider, useAIValueExportContext } from './export_provider'; +import { useKibana } from '../../../common/lib/kibana'; +import { AIValueReportEventTypes } from '../../../common/lib/telemetry/events/ai_value_report/types'; jest.mock('react-router', () => { return { useHistory: jest.fn(), }; }); + +jest.mock('../../../common/lib/kibana', () => { + return { + useKibana: jest.fn(), + }; +}); const useHistoryMock = useHistory as jest.Mock; +const useKibanaMock = useKibana as jest.Mock; +const reportEventMock = jest.fn(); + type ContextValue = ReturnType; const reportInput = { @@ -59,6 +70,7 @@ const TestComponent = ({ contextValueFn }: { contextValueFn: (context: ContextVa return ( <> {context?.isInsightVerified ? 'Insight verified' : ''} + {context?.shouldRegenerateInsight ? 'Insight should regenerate' : ''} {context?.buildForwardedState({ timeRange }) ? 'buildForwardedState available' : ''} @@ -86,6 +98,8 @@ describe('AIValueExportContext', () => { }; const verifyInsight = () => waitFor(() => screen.getByText('Insight verified')); + const verifyInsightShouldRegenerate = () => + waitFor(() => screen.getByText('Insight should regenerate')); const verifyBuildForwardedStateFnAvailable = () => waitFor(() => screen.getByText('buildForwardedState available')); @@ -93,9 +107,18 @@ describe('AIValueExportContext', () => { const setInsight = (insight: string) => act(() => context?.setInsight(insight)); beforeEach(() => { + jest.clearAllMocks() Object.defineProperties(global, { crypto: { value: webcrypto, writable: true }, }); + + useKibanaMock.mockReturnValue({ + services: { + telemetry: { + reportEvent: reportEventMock, + }, + }, + }); }); const forwardedState = { @@ -122,6 +145,22 @@ describe('AIValueExportContext', () => { it('should indicate that the insight should NOT be regenerated', () => { expect(context?.shouldRegenerateInsight).toBe(false); }); + + it('should report a telemetry event indicating that the report is being exported', () => { + expect(reportEventMock).toHaveBeenCalledWith( + AIValueReportEventTypes.AIValueReportExportExecution, + {} + ); + }); + + it('should report a telemetry event indicating that the insight should not be regenerated', () => { + expect(reportEventMock).toHaveBeenCalledWith( + AIValueReportEventTypes.AIValueReportExportInsightVerified, + { + shouldRegenerate: false, + } + ); + }); }); describe('when there is a forwarded state is valid and the report input is different', () => { @@ -130,6 +169,7 @@ describe('AIValueExportContext', () => { setReportInput({ ...reportInput, minutesPerAlert: 12345 }); await verifyInsight(); }); + it('should verify the insight', () => { expect(context?.isInsightVerified).toBe(true); }); @@ -137,6 +177,15 @@ describe('AIValueExportContext', () => { it('should indicate that the insight should be regenerated', () => { expect(context?.shouldRegenerateInsight).toBe(true); }); + + it('should report a telemetry event indicating that the insight should be regenerated', () => { + expect(reportEventMock).toHaveBeenCalledWith( + AIValueReportEventTypes.AIValueReportExportInsightVerified, + { + shouldRegenerate: true, + } + ); + }); }); describe('when the forwarded state is invalid', () => { @@ -147,25 +196,94 @@ describe('AIValueExportContext', () => { expect(context?.forwardedState).toBe(undefined); }); }); + + describe('when hashing returns an error', () => { + const errorMessage = 'Boom while hashing!'; + beforeEach(async () => { + Object.defineProperties(global, { + crypto: { + value: { + subtle: { + digest: () => { + throw Error(errorMessage); + }, + }, + }, + writable: true, + }, + }); + doRender(forwardedState); + setReportInput(reportInput); + await verifyInsight(); + }); + + it('should report a telemetry event with the error', () => { + expect(reportEventMock).toHaveBeenCalledWith( + AIValueReportEventTypes.AIValueReportExportError, + { + errorMessage, + isExportMode: true, + } + ); + }); + + it('should indicate that the insight should be regenerated', async () => { + await verifyInsightShouldRegenerate(); + }); + }); }); describe("normal mode: the page is rendered in the user's browser", () => { - beforeEach(() => { - doRender(undefined); + describe('happy path', () => { + beforeEach(() => { + doRender(undefined); + }); + + it('should expose a buildForwardedState that is defined only when the insight and the report input has loaded', async () => { + const buildState = () => context?.buildForwardedState({ timeRange }); + expect(buildState()).toBeUndefined(); + setInsight('Some valuable insight'); + expect(buildState()).toBeUndefined(); + setReportInput(reportInput); + await verifyBuildForwardedStateFnAvailable(); + + expect(buildState()).toEqual({ + timeRange, + insight: 'Some valuable insight', + reportDataHash, + }); + }); }); - it('should expose a buildForwardedState that is defined only when the insight and the report input has loaded', async () => { - const buildState = () => context?.buildForwardedState({ timeRange }); - expect(buildState()).toBeUndefined(); - setInsight('Some valuable insight'); - expect(buildState()).toBeUndefined(); - setReportInput(reportInput); - await verifyBuildForwardedStateFnAvailable(); - - expect(buildState()).toEqual({ - timeRange, - insight: 'Some valuable insight', - reportDataHash, + describe('when hashing returns an error', () => { + const errorMessage = 'Boom while hashing!'; + beforeEach(async () => { + Object.defineProperties(global, { + crypto: { + value: { + subtle: { + digest: () => { + throw Error(errorMessage); + }, + }, + }, + writable: true, + }, + }); + doRender(undefined); + setReportInput(reportInput); + }); + + it('should report a telemetry event with the error', async () => { + waitFor(() => { + expect(reportEventMock).toHaveBeenCalledWith( + AIValueReportEventTypes.AIValueReportExportError, + { + errorMessage, + isExportMode: false, + } + ); + }) }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx index 82c0a321adc38..7a4d2ca308439 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx @@ -5,10 +5,20 @@ * 2.0. */ -import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useHistory } from 'react-router-dom'; import type { ForwardedAIValueReportState } from '../../../../common/locators/ai_value_report/locator'; import { parseLocationState } from '../../../../common/locators/ai_value_report/locator'; +import { useKibana } from '../../../common/lib/kibana'; +import { AIValueReportEventTypes } from '../../../common/lib/telemetry/events/ai_value_report/types'; interface AIValueExportContext { forwardedState?: ForwardedAIValueReportState; @@ -67,6 +77,10 @@ export function AIValueExportProvider({ children }: AIValueExportProviderProps) const [reportDataHash, setReportDataHash] = useState(); const [insight, setInsight] = useState(); + const abortControllerRef = useRef(null); + const [hashReportErrorMessage, setHashReportErrorMessage] = useState(''); + const { telemetry } = useKibana().services; + const isExportMode = forwardedState !== undefined; useEffect(() => { if (history.location.state) { @@ -77,8 +91,25 @@ export function AIValueExportProvider({ children }: AIValueExportProviderProps) useEffect(() => { if (reportInput) { setReportDataHash(undefined); + if (abortControllerRef.current !== null) { + abortControllerRef.current.abort(); + } + const controller = new AbortController(); + abortControllerRef.current = controller; const generateReportDataHash = async () => { - setReportDataHash(await hashReportData(reportInput)); + let hash: string; + try { + hash = await hashReportData(reportInput); + } catch (e) { + // Fallback to the date string which will force the regeneration of the insight + hash = new Date().toISOString(); + setHashReportErrorMessage(e?.message ?? 'error during the hash generation'); + } + + if (controller.signal.aborted) { + return; + } + setReportDataHash(hash); }; generateReportDataHash(); @@ -92,6 +123,30 @@ export function AIValueExportProvider({ children }: AIValueExportProviderProps) } }, [forwardedState, reportDataHash]); + // Telemetry reporting + useEffect(() => { + if (isInsightVerified && shouldRegenerateInsight !== undefined) { + telemetry.reportEvent(AIValueReportEventTypes.AIValueReportExportInsightVerified, { + shouldRegenerate: shouldRegenerateInsight, + }); + } + }, [telemetry, isInsightVerified, shouldRegenerateInsight]); + + useEffect(() => { + if (isExportMode) { + telemetry.reportEvent(AIValueReportEventTypes.AIValueReportExportExecution, {}); + } + }, [isExportMode, telemetry]); + + useEffect(() => { + if (hashReportErrorMessage) { + telemetry.reportEvent(AIValueReportEventTypes.AIValueReportExportError, { + errorMessage: hashReportErrorMessage, + isExportMode, + }); + } + }, [hashReportErrorMessage, isExportMode, telemetry]); + const buildForwardedState = useCallback( ({ timeRange, From 4d15c2a4b174b11b9bd6e8caa5745c94cd4613d2 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 12:51:31 +0100 Subject: [PATCH 21/34] fix linting error --- .../public/reports/hooks/use_download_ai_value_report.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx index 4908ff41967d3..329861e8c9980 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/hooks/use_download_ai_value_report.tsx @@ -24,14 +24,15 @@ export const useDownloadAIValueReport = ({ const { share: shareService, serverless } = useKibana().services; const isServerless = !!serverless; const aiValueExportContext = useAIValueExportContext(); + const buildForwardedState = aiValueExportContext?.buildForwardedState; const forwardedState = useMemo(() => { - if (!aiValueExportContext?.buildForwardedState) { + if (!buildForwardedState) { return undefined; } - return aiValueExportContext.buildForwardedState({ timeRange }); - }, [timeRange, aiValueExportContext?.buildForwardedState]); + return buildForwardedState({ timeRange }); + }, [timeRange, buildForwardedState]); const isExportEnabled = forwardedState !== undefined && From eeea1913b291718edab24f9d7577b89527ceba79 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:20:05 +0000 Subject: [PATCH 22/34] Changes from node scripts/eslint_all_files --no-cache --fix --- .../security_solution/public/common/lib/telemetry/types.ts | 6 +++--- .../reports/providers/ai_value/export_provider.test.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts index 1be2726bedac3..10ca12eabef2d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/telemetry/types.ts @@ -51,10 +51,10 @@ import type { RuleUpgradeTelemetryEventsMap, } from './events/rule_upgrade/types'; -import { +import type { AIValueReportEventTypes, - AIValueReportTelemetryEventsMap -} from './events/ai_value_report/types' + AIValueReportTelemetryEventsMap, +} from './events/ai_value_report/types'; export * from './events/app/types'; export * from './events/alerts_grouping/types'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx index 9b1a9dcbac6ea..bf726a6a2514c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.test.tsx @@ -107,7 +107,7 @@ describe('AIValueExportContext', () => { const setInsight = (insight: string) => act(() => context?.setInsight(insight)); beforeEach(() => { - jest.clearAllMocks() + jest.clearAllMocks(); Object.defineProperties(global, { crypto: { value: webcrypto, writable: true }, }); @@ -283,7 +283,7 @@ describe('AIValueExportContext', () => { isExportMode: false, } ); - }) + }); }); }); }); From 0d2279f0bb32b27b35ad2164e549c1bad5aa408c Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 13:38:43 +0100 Subject: [PATCH 23/34] add isExportMode boolean to exportContext, hide title edit pencil in export mode --- .../reports/components/ai_value/cost_savings_metric.tsx | 2 +- .../reports/components/ai_value/executive_summary.tsx | 4 ++++ .../security_solution/public/reports/pages/ai_value.tsx | 2 +- .../public/reports/providers/ai_value/export_provider.tsx | 7 +++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx index 5cb8a455ad862..eca31bce8873c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx @@ -56,7 +56,7 @@ const CostSavingsMetricComponent: React.FC = ({ } = useEuiTheme(); const exportContext = useAIValueExportContext(); - const isExportMode = exportContext?.forwardedState !== undefined; + const isExportMode = exportContext?.isExportMode === true; const signalIndexName = useSignalIndexWithDefault(); const timerange = useMemo(() => ({ from, to }), [from, to]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx index f7afc5b78fc0f..97cb5cf3b8363 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx @@ -28,6 +28,7 @@ import type { ValueMetrics } from './metrics'; import { TimeSaved } from './time_saved'; import { FilteringRate } from './filtering_rate'; import { ThreatsDetected } from './threats_detected'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { attackAlertIds: string[]; @@ -69,6 +70,8 @@ export const ExecutiveSummary: React.FC = ({ }, [updateTitle] ); + const aiValueExportContext = useAIValueExportContext(); + const isExportMode = aiValueExportContext?.isExportMode === true; const subtitle = useMemo(() => { const fromDate = new Date(from); const toDate = new Date(to); @@ -105,6 +108,7 @@ export const ExecutiveSummary: React.FC = ({ `} > { const exportContext = useAIValueExportContext(); - const isExportMode = exportContext?.forwardedState !== undefined; + const isExportMode = exportContext?.isExportMode === true; const { loading: oldIsSourcererLoading } = useSourcererDataView(); const { from, to } = useDeepEqualSelector((state) => pick(['from', 'to'], inputsSelectors.valueReportTimeRangeSelector(state)) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx index 7a4d2ca308439..7214ed5e7d742 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/providers/ai_value/export_provider.tsx @@ -22,6 +22,7 @@ import { AIValueReportEventTypes } from '../../../common/lib/telemetry/events/ai interface AIValueExportContext { forwardedState?: ForwardedAIValueReportState; + isExportMode: boolean; isInsightVerified: boolean; shouldRegenerateInsight?: boolean; setReportInput: (inputData: object) => void; @@ -80,11 +81,12 @@ export function AIValueExportProvider({ children }: AIValueExportProviderProps) const abortControllerRef = useRef(null); const [hashReportErrorMessage, setHashReportErrorMessage] = useState(''); const { telemetry } = useKibana().services; - const isExportMode = forwardedState !== undefined; + const [isExportMode, setIsExportMode] = useState(false); useEffect(() => { if (history.location.state) { setForwardedState(parseLocationState(history.location.state)); + setIsExportMode(true); } }, [history.location.state]); @@ -166,6 +168,7 @@ export function AIValueExportProvider({ children }: AIValueExportProviderProps) const value = useMemo( () => ({ + isExportMode, forwardedState, isInsightVerified, shouldRegenerateInsight, @@ -173,7 +176,7 @@ export function AIValueExportProvider({ children }: AIValueExportProviderProps) setInsight, setReportInput, }), - [forwardedState, buildForwardedState, isInsightVerified, shouldRegenerateInsight] + [forwardedState, buildForwardedState, isInsightVerified, shouldRegenerateInsight, isExportMode] ); return {children}; From 7e9df736e8832af73ce3146a2fe0958ddf340c2b Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:22:09 +0000 Subject: [PATCH 24/34] Changes from node scripts/regenerate_moon_projects.js --update --- packages/kbn-check-saved-objects-cli/moon.yml | 3 +++ x-pack/solutions/observability/test/moon.yml | 3 --- x-pack/solutions/security/test/moon.yml | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kbn-check-saved-objects-cli/moon.yml b/packages/kbn-check-saved-objects-cli/moon.yml index be7edc021ab8f..e5794979cd906 100644 --- a/packages/kbn-check-saved-objects-cli/moon.yml +++ b/packages/kbn-check-saved-objects-cli/moon.yml @@ -35,6 +35,9 @@ dependsOn: - '@kbn/core-saved-objects-server-internal' - '@kbn/core-root-server-internal' - '@kbn/core-test-helpers-so-type-serializer' + - '@kbn/core-saved-objects-api-server' + - '@kbn/migrator-test-kit' + - '@kbn/config-schema' tags: - shared-common - package diff --git a/x-pack/solutions/observability/test/moon.yml b/x-pack/solutions/observability/test/moon.yml index 6fa817d2b1850..c1554db1e2f73 100644 --- a/x-pack/solutions/observability/test/moon.yml +++ b/x-pack/solutions/observability/test/moon.yml @@ -84,9 +84,6 @@ dependsOn: - '@kbn/observability-ai-assistant-app-plugin' - '@kbn/dev-utils' - '@kbn/serverless-observability-settings' - - '@kbn/cypress-config' - - '@kbn/dev-proc-runner' - - '@kbn/cypress-test-helper' - '@kbn/streams-schema' - '@kbn/data-quality' - '@kbn/onechat-common' diff --git a/x-pack/solutions/security/test/moon.yml b/x-pack/solutions/security/test/moon.yml index 2b32f900a757f..67af1f662b975 100644 --- a/x-pack/solutions/security/test/moon.yml +++ b/x-pack/solutions/security/test/moon.yml @@ -53,6 +53,7 @@ dependsOn: - '@kbn/management-settings-ids' - '@kbn/connector-schemas' - '@kbn/cloud-security-posture-graph' + - '@kbn/securitysolution-list-constants' tags: - test-helper - package From 042035cdf50da56b93a86e75dcd4e699bdc6c27b Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 14:53:24 +0100 Subject: [PATCH 25/34] make valueReport non optional in UrlInputsModel --- .../security_solution/public/common/store/inputs/model.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/solutions/security/plugins/security_solution/public/common/store/inputs/model.ts index 5342c4afc79d5..239e92230343b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/store/inputs/model.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/store/inputs/model.ts @@ -102,8 +102,7 @@ export interface UrlInputsModelInputs { export interface UrlInputsModel { global: UrlInputsModelInputs; timeline: UrlInputsModelInputs; - // serverless only - valueReport?: UrlInputsModelInputs; + valueReport: UrlInputsModelInputs; // TODO: remove ? when isSocTrendsEnabled feature flag is removed socTrends?: UrlInputsModelInputs; } From 0dc225e2af3acdeffcb3bacb7304a2f0a6f482fc Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 14:53:39 +0100 Subject: [PATCH 26/34] use loading state for export button --- .../plugins/security_solution/public/reports/pages/ai_value.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx index 032b125e70639..b3a7a7f198840 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.tsx @@ -102,7 +102,7 @@ const BaseComponent = () => { size="s" aria-label={EXPORT_REPORT} onClick={toggleContextMenu} - isDisabled={!isExportEnabled} + isLoading={!isExportEnabled} > {EXPORT_REPORT} From 34d24314512752e366db9d8e54a8056a1552b0d9 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 15:46:57 +0100 Subject: [PATCH 27/34] fix type issues related to valueReport being now always available in url params --- .../common/hooks/search_bar/use_sync_timerange_url_param.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts index b52f249808795..7e8e39f6a8d35 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts @@ -35,7 +35,6 @@ export const useSyncTimerangeUrlParam = () => { }, [inputState.socTrends, isSocTrendsEnabled]); const valueReportUrlParams = useMemo(() => { - if (inputState.valueReport) { const { linkTo: valueReportLinkTo, timerange: valueReportTimerange } = inputState.valueReport; return { valueReport: { @@ -43,8 +42,6 @@ export const useSyncTimerangeUrlParam = () => { linkTo: valueReportLinkTo, }, }; - } - return {}; }, [inputState.valueReport]); useEffect(() => { From e8c7e19978221f84b3fc93cfc2cee46ec73b402e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:19:06 +0000 Subject: [PATCH 28/34] Changes from node scripts/eslint_all_files --no-cache --fix --- .../search_bar/use_sync_timerange_url_param.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts index 7e8e39f6a8d35..9757673c7884c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/search_bar/use_sync_timerange_url_param.ts @@ -35,13 +35,13 @@ export const useSyncTimerangeUrlParam = () => { }, [inputState.socTrends, isSocTrendsEnabled]); const valueReportUrlParams = useMemo(() => { - const { linkTo: valueReportLinkTo, timerange: valueReportTimerange } = inputState.valueReport; - return { - valueReport: { - [URL_PARAM_KEY.timerange]: valueReportTimerange, - linkTo: valueReportLinkTo, - }, - }; + const { linkTo: valueReportLinkTo, timerange: valueReportTimerange } = inputState.valueReport; + return { + valueReport: { + [URL_PARAM_KEY.timerange]: valueReportTimerange, + linkTo: valueReportLinkTo, + }, + }; }, [inputState.valueReport]); useEffect(() => { From 7a02485148aaa7b6ff49c70982b1a4cc504dcff8 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 16:41:55 +0100 Subject: [PATCH 29/34] adjust the ValueReportSettingsComponent: reword the change rate cta in export mode --- .../components/ai_value/translations.ts | 7 ++++ .../ai_value/value_report_settings.tsx | 35 ++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts index 72aea8873103f..a0859a710e899 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts @@ -207,6 +207,13 @@ export const CHANGE_RATE = i18n.translate('xpack.securitySolution.reports.aiValu defaultMessage: 'Change rate in advanced settings', }); +export const CHANGE_RATE_EXPORT_MODE = i18n.translate( + 'xpack.securitySolution.reports.aiValue.exportMode.changeRate', + { + defaultMessage: 'You can change the rate in advanced settings.', + } +); + export const EDIT_TITLE = i18n.translate('xpack.securitySolution.reports.aiValue.editTitle', { defaultMessage: 'Edit title inline', }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/value_report_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/value_report_settings.tsx index d0aeb35eb7790..239949cec0d52 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/value_report_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/value_report_settings.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiText, EuiLink, EuiIcon, useEuiTheme, EuiTitle, EuiSpacer } from '@elastic/eui'; import { useNavigation } from '@kbn/security-solution-navigation'; import { css } from '@emotion/react'; import * as i18n from './translations'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { minutesPerAlert: number; @@ -26,6 +27,26 @@ const ValueReportSettingsComponent: React.FC = ({ minutesPerAlert, analys () => navigateTo({ appId: 'management', path: '/kibana/settings?query=defaultValueReport' }), [navigateTo] ); + const aiValueExportContext = useAIValueExportContext(); + const isExportMode = aiValueExportContext?.isExportMode === true; + const changeRateLink = useMemo(() => { + if (isExportMode) { + return {i18n.CHANGE_RATE_EXPORT_MODE}; + } + + return ( + + {i18n.CHANGE_RATE} + + + ); + }, [isExportMode, goToKibanaSettings]); return (
= ({ minutesPerAlert, analys

- {i18n.COST_CALCULATION({ minutesPerAlert, analystHourlyRate })}{' '} - - {i18n.CHANGE_RATE} - - + {i18n.COST_CALCULATION({ minutesPerAlert, analystHourlyRate })} {changeRateLink}

{i18n.LEGAL_DISCLAIMER}

From 076712c20d8ab05ea81e89b91205109fcaa90b14 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Tue, 25 Nov 2025 16:43:04 +0100 Subject: [PATCH 30/34] adjust copy --- .../public/reports/components/ai_value/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts index a0859a710e899..30a626891a3bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/translations.ts @@ -210,7 +210,7 @@ export const CHANGE_RATE = i18n.translate('xpack.securitySolution.reports.aiValu export const CHANGE_RATE_EXPORT_MODE = i18n.translate( 'xpack.securitySolution.reports.aiValue.exportMode.changeRate', { - defaultMessage: 'You can change the rate in advanced settings.', + defaultMessage: 'Value report rates configured in advanced settings.', } ); From 48dd736b37cea6b70ea5c7529ccc49f12711f995 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Wed, 26 Nov 2025 15:17:56 +0100 Subject: [PATCH 31/34] miscellaneous UI improvements --- .../ai_value/alert_filtering_metric.tsx | 5 +- .../ai_value/alert_processing_key_insight.tsx | 1 + .../ai_value/compare_percentage.tsx | 2 +- .../ai_value/cost_savings_key_insight.tsx | 8 +- .../ai_value/cost_savings_metric.tsx | 85 ++++++++++--------- .../components/ai_value/executive_summary.tsx | 2 +- .../ai_value/threats_detected_metric.tsx | 5 +- .../components/ai_value/time_saved_metric.tsx | 5 +- 8 files changed, 65 insertions(+), 48 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx index 609c167a64c4b..39859811d98fc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx @@ -17,6 +17,7 @@ import { VisualizationContextMenuActions } from '../../../common/components/visu import { getAlertFilteringMetricLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/ai/alert_filtering_metric'; import * as i18n from './translations'; import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { attackAlertIds: string[]; @@ -34,6 +35,8 @@ const AlertFilteringMetricComponent: React.FC = ({ const { euiTheme: { colors }, } = useEuiTheme(); + const aiValueExportContext = useAIValueExportContext(); + const isExportMode = aiValueExportContext?.isExportMode === true; const extraVisualizationOptions = useMemo( () => ({ filters: getExcludeAlertsFilters(attackAlertIds), @@ -53,7 +56,7 @@ const AlertFilteringMetricComponent: React.FC = ({ height: 100% !important; } .echMetricText__icon .euiIcon { - fill: ${colors.vis.euiColorVis4}; + ${isExportMode ? 'display: none;' : `fill: ${colors.vis.euiColorVis4};`} } .echMetricText { padding: 8px 16px 60px; diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_processing_key_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_processing_key_insight.tsx index 5e8ff4c484a15..73c4aa6402858 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_processing_key_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_processing_key_insight.tsx @@ -76,6 +76,7 @@ export const AlertProcessingKeyInsight: React.FC = ({ isLoading, valueMet css={css` line-height: 1.6em; `} + color="subdued" >
  • - + {percentInfo.note} {` `} {i18n.TIME_RANGE(timeRange)} diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx index e7fda4caaab10..c72aad1fcf164 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.tsx @@ -87,7 +87,7 @@ interface ViewProps { const CostSavingsKeyInsightView: React.FC = ({ insight, isLoading }) => { const { - euiTheme: { size }, + euiTheme: { size, colors }, } = useEuiTheme(); return (
    = ({ insight, isLoading }) border-radius: ${size.s}; padding: ${size.base}; min-height: 200px; + + .keyInsightMarkdown { + color: ${colors.textSubdued}; + } `} > @@ -123,7 +127,7 @@ const CostSavingsKeyInsightView: React.FC = ({ insight, isLoading }) {insight && !isLoading ? ( - + ) : ( )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx index eca31bce8873c..00bacc992adc1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.tsx @@ -72,47 +72,50 @@ const CostSavingsMetricComponent: React.FC = ({ [analystHourlyRate, colors.backgroundBaseSuccess, minutesPerAlert, signalIndexName] ); - const Visualization = ( -
    * { - height: 100% !important; - } - .echMetricText__icon .euiIcon { - fill: ${colors.success}; - } - .echMetricText { - padding: 8px 16px 60px; - } - p.echMetricText__value { - color: ${colors.success}; - font-size: 48px !important; - padding: 10px 0; - } - .euiPanel, - .embPanel__hoverActions > span { - background: ${colors.backgroundBaseSuccess}; - } - .embPanel__hoverActionsAnchor { - --internalBorderStyle: 1px solid ${colors.success}!important; - } - `} - > - -
    + const Visualization = useMemo( + () => ( +
    * { + height: 100% !important; + } + .echMetricText__icon .euiIcon { + fill: ${colors.success}; + } + .echMetricText { + padding: 8px 16px 60px; + } + p.echMetricText__value { + color: ${colors.success}; + font-size: 48px !important; + padding: 10px 0; + } + .euiPanel, + .embPanel__hoverActions > span { + background: ${colors.backgroundBaseSuccess}; + } + .embPanel__hoverActionsAnchor { + --internalBorderStyle: 1px solid ${colors.success}!important; + } + `} + > + +
    + ), + [getLensAttributes, timerange, colors.success, colors.backgroundBaseSuccess] ); if (isExportMode) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx index 97cb5cf3b8363..7d6b179b72a40 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/executive_summary.tsx @@ -138,7 +138,7 @@ export const ExecutiveSummary: React.FC = ({ data-test-subj="executiveSummaryMainInfo" > - + {isLoading ? ( ) : hasAttackDiscoveries ? ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx index 70723abd59a8a..abd3ec1df9f0b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx @@ -14,6 +14,7 @@ import { useSpaceId } from '../../../common/hooks/use_space_id'; import * as i18n from './translations'; import { getThreatsDetectedMetricLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/ai/threats_detected_metric'; import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { from: string; @@ -24,6 +25,8 @@ const ThreatsDetectedMetricComponent: React.FC = ({ from, to }) => { const { euiTheme: { colors }, } = useEuiTheme(); + const aiValueExportContext = useAIValueExportContext(); + const isExportMode = aiValueExportContext?.isExportMode === true; const spaceId = useSpaceId(); return ( @@ -34,7 +37,7 @@ const ThreatsDetectedMetricComponent: React.FC = ({ from, to }) => { height: 100% !important; } .echMetricText__icon .euiIcon { - fill: ${colors.vis.euiColorVis6}; + ${isExportMode ? 'display: none;' : `fill: ${colors.vis.euiColorVis6};`} } .echMetricText { padding: 8px 16px 60px; diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx index 93e809babf364..4d7896775c144 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx @@ -18,6 +18,7 @@ import { import { getTimeSavedMetricLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/ai/time_saved_metric'; import * as i18n from './translations'; import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable'; +import { useAIValueExportContext } from '../../providers/ai_value/export_provider'; interface Props { from: string; @@ -36,6 +37,8 @@ const TimeSavedMetricComponent: React.FC = ({ from, to, minutesPerAlert } } = useEuiTheme(); const timerange = useMemo(() => ({ from, to }), [from, to]); const signalIndexName = useSignalIndexWithDefault(); + const aiValueExportContext = useAIValueExportContext(); + const isExportMode = aiValueExportContext?.isExportMode === true; const getLensAttributes = useCallback( (args) => getTimeSavedMetricLensAttributes({ ...args, minutesPerAlert, signalIndexName }), @@ -50,7 +53,7 @@ const TimeSavedMetricComponent: React.FC = ({ from, to, minutesPerAlert } height: 100% !important; } .echMetricText__icon .euiIcon { - fill: ${colors.vis.euiColorVis2}; + ${isExportMode ? 'display: none;' : `fill: ${colors.vis.euiColorVis2};`} } .echMetricText { padding: 8px 16px 60px; From 323ba8e0eaf152b2b01668a3ba23a90d77433373 Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Wed, 26 Nov 2025 15:33:04 +0100 Subject: [PATCH 32/34] fix unit tests --- .../components/ai_value/cost_savings_key_insight.test.tsx | 2 +- .../reports/components/ai_value/cost_savings_metric.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx index f0b4a7fdc9860..1d3a74611f187 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_key_insight.test.tsx @@ -186,7 +186,7 @@ describe('CostSavingsKeyInsight', () => { render(, { wrapper }); await waitFor(() => { - expect(mockSetInsightInExportContext).toHaveBeenCalledWith(''); + expect(mockSetInsightInExportContext).toHaveBeenCalledWith('Test result'); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx index ab6cd40c552c7..adb690809cb12 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/cost_savings_metric.test.tsx @@ -84,7 +84,7 @@ describe('CostSavingsMetric', () => { beforeEach(() => { // Force it into export mode useAIValueExportContextMock.mockReturnValue({ - forwardedState: {}, + isExportMode: true, }); }); From 3709a8a591b7e8064e1bb660be64aaa9fbe9173d Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Thu, 27 Nov 2025 10:19:06 +0100 Subject: [PATCH 33/34] fix unit test --- .../security_solution/public/reports/pages/ai_value.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx index 089bf4eaeec1b..4b2701e9b2323 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/pages/ai_value.test.tsx @@ -347,6 +347,7 @@ describe('AIValue', () => { to: '2025-01-31T23:59:59.999Z', }, }, + isExportMode: true, }); }); From 1554f8a136a3fc10b1f4ee42f5bf80943cf6710f Mon Sep 17 00:00:00 2001 From: Edgar Santos Date: Thu, 27 Nov 2025 10:19:59 +0100 Subject: [PATCH 34/34] further ui adjustments --- .../reports/components/ai_value/alert_filtering_metric.tsx | 3 +++ .../reports/components/ai_value/threats_detected_metric.tsx | 3 +++ .../public/reports/components/ai_value/time_saved_metric.tsx | 3 +++ 3 files changed, 9 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx index 39859811d98fc..c707c480930b0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/alert_filtering_metric.tsx @@ -58,6 +58,9 @@ const AlertFilteringMetricComponent: React.FC = ({ .echMetricText__icon .euiIcon { ${isExportMode ? 'display: none;' : `fill: ${colors.vis.euiColorVis4};`} } + .echMetricText__valueBlock { + grid-row-start: 3 !important; + } .echMetricText { padding: 8px 16px 60px; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx index abd3ec1df9f0b..ac9be4b347828 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/threats_detected_metric.tsx @@ -39,6 +39,9 @@ const ThreatsDetectedMetricComponent: React.FC = ({ from, to }) => { .echMetricText__icon .euiIcon { ${isExportMode ? 'display: none;' : `fill: ${colors.vis.euiColorVis6};`} } + .echMetricText__valueBlock { + grid-row-start: 3 !important; + } .echMetricText { padding: 8px 16px 60px; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx index 4d7896775c144..34fcfd5cd2ed2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/reports/components/ai_value/time_saved_metric.tsx @@ -55,6 +55,9 @@ const TimeSavedMetricComponent: React.FC = ({ from, to, minutesPerAlert } .echMetricText__icon .euiIcon { ${isExportMode ? 'display: none;' : `fill: ${colors.vis.euiColorVis2};`} } + .echMetricText__valueBlock { + grid-row-start: 3 !important; + } .echMetricText { padding: 8px 16px 60px; }