diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 5956c69f1eda2..bf5ff2d7bcb9c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -2298,6 +2298,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
/x-pack/solutions/security/plugins/security_solution/common/timelines @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/components/alerts_viewer @elastic/security-threat-hunting-investigations
+/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_action @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer @elastic/security-threat-hunting-investigations
@@ -2317,6 +2318,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
/x-pack/solutions/security/plugins/security_solution/public/flyout/rule_details @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/investigations @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections @elastic/security-threat-hunting-investigations
+/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detections/alert_summary @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx @elastic/security-threat-hunting-investigations
/x-pack/solutions/security/plugins/security_solution/public/common/components/drag_and_drop @elastic/security-threat-hunting-investigations
diff --git a/x-pack/platform/plugins/shared/fleet/public/index.ts b/x-pack/platform/plugins/shared/fleet/public/index.ts
index d82e9c88b7db8..9d6d6278da810 100644
--- a/x-pack/platform/plugins/shared/fleet/public/index.ts
+++ b/x-pack/platform/plugins/shared/fleet/public/index.ts
@@ -10,6 +10,7 @@ import type { PluginInitializerContext } from '@kbn/core/public';
import { lazy } from 'react';
import { FleetPlugin } from './plugin';
+
export type { GetPackagesResponse } from './types';
export { installationStatuses } from '../common/constants';
@@ -89,3 +90,6 @@ export const AvailablePackagesHook = () => {
'./applications/integrations/sections/epm/screens/home/hooks/use_available_packages'
);
};
+
+export { useGetPackagesQuery } from './hooks/use_request/epm';
+export { useGetSettingsQuery } from './hooks/use_request/settings';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts
index 5f62097b0d20c..61f3e1ef7f532 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts
@@ -112,6 +112,10 @@ export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts',
defaultMessage: 'Alerts',
});
+export const ALERT_SUMMARY = i18n.translate('xpack.securitySolution.navigation.alertSummary', {
+ defaultMessage: 'Alert summary',
+});
+
export const ATTACK_DISCOVERY = i18n.translate(
'xpack.securitySolution.navigation.attackDiscovery',
{
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx
index a8140042a2042..8db6d69faf5ad 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx
@@ -13,21 +13,17 @@ import { SecurityPageName } from '../../../../common';
import { TestProviders } from '../../mock';
import { generateHistoryMock } from '../../utils/route/mocks';
import type { LinkInfo } from '../../links';
+import { useLinkInfo } from '../../links';
+import { useUpsellingPage } from '../../hooks/use_upselling';
+
+jest.mock('../../links');
+jest.mock('../../hooks/use_upselling');
const defaultLinkInfo: LinkInfo = {
id: SecurityPageName.exploreLanding,
title: 'test',
path: '/test',
};
-const mockGetLink = jest.fn((): LinkInfo | undefined => defaultLinkInfo);
-jest.mock('../../links', () => ({
- useLinkInfo: () => mockGetLink(),
-}));
-
-const mockUseUpsellingPage = jest.fn();
-jest.mock('../../hooks/use_upselling', () => ({
- useUpsellingPage: () => mockUseUpsellingPage(),
-}));
const REDIRECT_COMPONENT_SUBJ = 'redirect-component';
const mockRedirect = jest.fn(() =>
);
@@ -47,8 +43,17 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
);
describe('SecurityRoutePageWrapper', () => {
- it('should render children when authorized', () => {
- mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo }); // authorized
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render UpsellPage when it is available', () => {
+ const TEST_ID = 'test-upsell-page';
+ const TestUpsellPage = () => ;
+
+ (useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo);
+ (useUpsellingPage as jest.Mock).mockReturnValue(TestUpsellPage);
+
const { getByTestId } = render(
@@ -56,14 +61,55 @@ describe('SecurityRoutePageWrapper', () => {
{ wrapper: Wrapper }
);
- expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
+ expect(getByTestId(TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should redirect when link missing and redirectOnMissing flag present', () => {
+ (useLinkInfo as jest.Mock).mockReturnValue(undefined);
+ (useUpsellingPage as jest.Mock).mockReturnValue(undefined);
+
+ const { getByTestId } = render(
+
+
+ ,
+ { wrapper: Wrapper }
+ );
+
+ expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
+ });
+
+ it('should redirect when link missing and redirectIfUnauthorized flag present', () => {
+ (useLinkInfo as jest.Mock).mockReturnValue(undefined);
+ (useUpsellingPage as jest.Mock).mockReturnValue(undefined);
+
+ const { getByTestId } = render(
+
+
+ ,
+ { wrapper: Wrapper }
+ );
+
+ expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
});
- it('should render UpsellPage when unauthorized and UpsellPage is available', () => {
- const TestUpsellPage = () => ;
+ it('should redirect when link is unauthorized and redirectIfUnauthorized flag present', () => {
+ (useLinkInfo as jest.Mock).mockReturnValue({ ...defaultLinkInfo, unauthorized: true });
+ (useUpsellingPage as jest.Mock).mockReturnValue(undefined);
+
+ const { getByTestId } = render(
+
+
+ ,
+ { wrapper: Wrapper }
+ );
+
+ expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
+ });
+
+ it('should render NoPrivilegesPage when link missing and UpsellPage is undefined', () => {
+ (useLinkInfo as jest.Mock).mockReturnValue(undefined);
+ (useUpsellingPage as jest.Mock).mockReturnValue(undefined);
- mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
- mockUseUpsellingPage.mockReturnValue(TestUpsellPage);
const { getByTestId } = render(
@@ -71,12 +117,13 @@ describe('SecurityRoutePageWrapper', () => {
{ wrapper: Wrapper }
);
- expect(getByTestId('test-upsell-page')).toBeInTheDocument();
+ expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
});
- it('should render NoPrivilegesPage when unauthorized and UpsellPage is unavailable', () => {
- mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
- mockUseUpsellingPage.mockReturnValue(undefined);
+ it('should render NoPrivilegesPage when unauthorized and UpsellPage is undefined', () => {
+ (useLinkInfo as jest.Mock).mockReturnValue({ ...defaultLinkInfo, unauthorized: true });
+ (useUpsellingPage as jest.Mock).mockReturnValue(undefined);
+
const { getByTestId } = render(
@@ -87,16 +134,17 @@ describe('SecurityRoutePageWrapper', () => {
expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
});
- it('should redirect when link missing and redirectOnMissing flag present', () => {
- mockGetLink.mockReturnValueOnce(undefined);
+ it('should render children when authorized', () => {
+ (useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo);
+ (useUpsellingPage as jest.Mock).mockReturnValue(undefined);
const { getByTestId } = render(
-
+
,
{ wrapper: Wrapper }
);
- expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
+ expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
});
});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx
index 3351666c64529..aca57b1726083 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx
@@ -18,6 +18,10 @@ import { SpyRoute } from '../../utils/route/spy_routes';
interface SecurityRoutePageWrapperProps {
pageName: SecurityPageName;
redirectOnMissing?: boolean;
+ /**
+ * Used primarily in the AI for SOC tier, to allow redirecting to the home page instead of showing the NoPrivileges page.
+ */
+ redirectIfUnauthorized?: boolean;
}
/**
@@ -40,13 +44,14 @@ interface SecurityRoutePageWrapperProps {
export const SecurityRoutePageWrapper: FC> = ({
children,
pageName,
+ redirectIfUnauthorized,
redirectOnMissing,
}) => {
const link = useLinkInfo(pageName);
- const UpsellingPage = useUpsellingPage(pageName);
- // The upselling page is only returned when the license/product requirements are not met,
+ // The upselling page is only returned when the license/product requirements are not met.
// When it is defined it must be rendered, no need to check anything else.
+ const UpsellingPage = useUpsellingPage(pageName);
if (UpsellingPage) {
return (
<>
@@ -56,28 +61,38 @@ export const SecurityRoutePageWrapper: FC;
+ }
+
const isAuthorized = link != null && !link.unauthorized;
- if (isAuthorized) {
+
+ // Allows a redirect to the home page if the link is undefined or unauthorized.
+ // This is used in the AI for SOC tier (for the Alert Summary page for example), as it does not make sense to show the NoPrivilegesPage.
+ if (redirectIfUnauthorized && !isAuthorized) {
+ return ;
+ }
+
+ // Show the no privileges page if the link is undefined or unauthorized.
+ if (!isAuthorized) {
return (
-
- {children}
+ <>
-
+ docLinks.siem.privileges}
+ />
+ >
);
}
- if (redirectOnMissing && link == null) {
- return ; // redirects to the home page
- }
-
+ // Show the actual application page.
return (
- <>
+
+ {children}
- docLinks.siem.privileges}
- />
- >
+
);
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/landing_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/landing_page.tsx
new file mode 100644
index 0000000000000..38a2836ba45ca
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/landing_page.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { EuiText } from '@elastic/eui';
+
+export const LANDING_PAGE_PROMPT_TEST_ID = 'alert-summary-landing-page-prompt';
+
+export interface LandingPageProps {
+ /**
+ * List of available AI for SOC integrations
+ */
+ packages: PackageListItem[];
+}
+
+/**
+ * Displays a gif of the alerts summary page, with empty prompt showing the top 2 available AI for SOC packages.
+ * This page is rendered when no AI for SOC packages are installed.
+ */
+export const LandingPage = memo(({ packages }: LandingPageProps) => {
+ return {'Landing page'};
+});
+
+LandingPage.displayName = 'LandingPage';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx
new file mode 100644
index 0000000000000..d6393d718dcd4
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import { EuiText } from '@elastic/eui';
+
+export const DATA_VIEW_LOADING_PROMPT_TEST_ID = 'alert-summary-data-view-loading-prompt';
+
+export interface WrapperProps {
+ /**
+ * List of installed Ai for SOC integrations
+ */
+ packages: PackageListItem[];
+}
+
+/**
+ * Creates a new dataView with the alert indices while displaying a loading skeleton.
+ * Display the alert summary page content if the dataView is correctly created.
+ * This page is rendered when there are AI for SOC packages installed.
+ */
+export const Wrapper = memo(({ packages }: WrapperProps) => {
+ return {'Wrapper'};
+});
+
+Wrapper.displayName = 'Wrapper';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.test.ts
new file mode 100644
index 0000000000000..f5805d315c778
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.test.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook } from '@testing-library/react';
+import { useFetchIntegrations } from './use_fetch_integrations';
+import { useKibana } from '../../../common/lib/kibana';
+import {
+ installationStatuses,
+ useGetPackagesQuery,
+ useGetSettingsQuery,
+} from '@kbn/fleet-plugin/public';
+
+jest.mock('../../../common/lib/kibana');
+jest.mock('@kbn/fleet-plugin/public');
+
+describe('useFetchIntegrations', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return isLoading true', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ fleet: {
+ authz: {
+ fleet: {
+ readSettings: true,
+ },
+ },
+ },
+ },
+ });
+ (useGetSettingsQuery as jest.Mock).mockReturnValue({
+ isFetchedAfterMount: true,
+ });
+ (useGetPackagesQuery as jest.Mock).mockReturnValue({
+ data: [],
+ isLoading: true,
+ });
+
+ const { result } = renderHook(() => useFetchIntegrations());
+
+ expect(result.current.availablePackage).toHaveLength(0);
+ expect(result.current.installedPackages).toHaveLength(0);
+ expect(result.current.isLoading).toBe(true);
+ });
+
+ it('should return availablePackage and installedPackages', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ fleet: {
+ authz: {
+ fleet: {
+ readSettings: true,
+ },
+ },
+ },
+ },
+ });
+ (useGetSettingsQuery as jest.Mock).mockReturnValue({
+ isFetchedAfterMount: true,
+ });
+ (useGetPackagesQuery as jest.Mock).mockReturnValue({
+ data: {
+ items: [
+ {
+ name: 'splunk',
+ status: installationStatuses.Installed,
+ },
+ {
+ name: 'google_secops',
+ status: installationStatuses.InstallFailed,
+ },
+ {
+ name: 'microsoft_sentinel',
+ status: installationStatuses.NotInstalled,
+ },
+ { name: 'unknown' },
+ ],
+ },
+ isLoading: false,
+ });
+
+ const { result } = renderHook(() => useFetchIntegrations());
+
+ expect(result.current.availablePackage).toHaveLength(1);
+ expect(result.current.availablePackage[0].name).toBe('microsoft_sentinel');
+
+ expect(result.current.installedPackages).toHaveLength(2);
+ expect(result.current.installedPackages[0].name).toBe('splunk');
+ expect(result.current.installedPackages[1].name).toBe('google_secops');
+
+ expect(result.current.isLoading).toBe(false);
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts
new file mode 100644
index 0000000000000..bc3cb7dddba54
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { useMemo } from 'react';
+import type { PackageListItem } from '@kbn/fleet-plugin/common';
+import {
+ installationStatuses,
+ useGetPackagesQuery,
+ useGetSettingsQuery,
+} from '@kbn/fleet-plugin/public';
+import { useKibana } from '../../../common/lib/kibana';
+
+// We hardcode these here for now as we currently do not have any other way to filter out all the unwanted integrations.
+const AI_FOR_SOC_INTEGRATIONS = [
+ 'splunk', // doesnt yet exist
+ 'google_secops',
+ 'microsoft_sentinel',
+ 'sentinel_one',
+ 'crowdstrike',
+];
+
+export interface UseFetchIntegrationsResult {
+ /**
+ * Is true while the data is loading
+ */
+ isLoading: boolean;
+ /**
+ * The AI for SOC installed integrations (see list in the constant above)
+ */
+ installedPackages: PackageListItem[];
+ /**
+ * The AI for SOC not-installed integrations (see list in the constant above)
+ */
+ availablePackage: PackageListItem[];
+}
+
+/**
+ * Fetches all integrations, then returns the installed and non-installed ones filtered with a list of
+ * hard coded AI for SOC integrations:
+ * - splunk
+ * - google_secops
+ * - microsoft_sentinel
+ * - sentinel_one
+ * - crowdstrike
+ */
+export const useFetchIntegrations = (): UseFetchIntegrationsResult => {
+ const { fleet } = useKibana().services;
+ const isAuthorizedToFetchSettings = fleet?.authz.fleet.readSettings;
+ const { isFetchedAfterMount: isSettingsFetched } = useGetSettingsQuery({
+ enabled: isAuthorizedToFetchSettings,
+ });
+ const shouldFetchPackages = !isAuthorizedToFetchSettings || isSettingsFetched;
+ const { data: allPackages, isLoading } = useGetPackagesQuery(
+ {
+ prerelease: true,
+ },
+ {
+ enabled: shouldFetchPackages,
+ }
+ );
+
+ const aiForSOCPackages: PackageListItem[] = useMemo(
+ () => (allPackages?.items || []).filter((pkg) => AI_FOR_SOC_INTEGRATIONS.includes(pkg.name)),
+ [allPackages]
+ );
+ const availablePackage: PackageListItem[] = useMemo(
+ () => aiForSOCPackages.filter((pkg) => pkg.status === installationStatuses.NotInstalled),
+ [aiForSOCPackages]
+ );
+ const installedPackages: PackageListItem[] = useMemo(
+ () =>
+ aiForSOCPackages.filter(
+ (pkg) =>
+ pkg.status === installationStatuses.Installed ||
+ pkg.status === installationStatuses.InstallFailed
+ ),
+ [aiForSOCPackages]
+ );
+
+ return useMemo(
+ () => ({
+ isLoading,
+ installedPackages,
+ availablePackage,
+ }),
+ [isLoading, installedPackages, availablePackage]
+ );
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts
index 982df1c5a6f8c..29679805cd12c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts
@@ -4,20 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { i18n } from '@kbn/i18n';
import {
+ ALERT_SUMMARY_PATH,
ALERTS_PATH,
- SecurityPageName,
SECURITY_FEATURE_ID,
- ALERT_SUMMARY_PATH,
+ SecurityPageName,
} from '../../common/constants';
-import { ALERTS } from '../app/translations';
+import { ALERT_SUMMARY, ALERTS } from '../app/translations';
import type { LinkItem } from '../common/links/types';
export const alertsLink: LinkItem = {
- id: SecurityPageName.alerts,
- title: ALERTS,
- path: ALERTS_PATH,
capabilities: [`${SECURITY_FEATURE_ID}.show`],
globalNavPosition: 3,
globalSearchKeywords: [
@@ -25,12 +23,12 @@ export const alertsLink: LinkItem = {
defaultMessage: 'Alerts',
}),
],
+ id: SecurityPageName.alerts,
+ path: ALERTS_PATH,
+ title: ALERTS,
};
export const alertSummaryLink: LinkItem = {
- id: SecurityPageName.alertSummary,
- path: ALERT_SUMMARY_PATH,
- title: 'Alert summary',
capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${SECURITY_FEATURE_ID}.alerts_summary`]],
globalNavPosition: 3,
globalSearchKeywords: [
@@ -39,4 +37,7 @@ export const alertSummaryLink: LinkItem = {
}),
],
hideTimeline: true,
+ id: SecurityPageName.alertSummary,
+ path: ALERT_SUMMARY_PATH,
+ title: ALERT_SUMMARY,
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx
new file mode 100644
index 0000000000000..90aafcacfbe46
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { AlertSummaryPage, LOADING_INTEGRATIONS_TEST_ID } from './alert_summary';
+import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations';
+import { LANDING_PAGE_PROMPT_TEST_ID } from '../../components/alert_summary/landing_page/landing_page';
+import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url';
+import { DATA_VIEW_LOADING_PROMPT_TEST_ID } from '../../components/alert_summary/wrapper';
+
+jest.mock('../../hooks/alert_summary/use_fetch_integrations');
+jest.mock('../../../common/hooks/use_add_integrations_url');
+
+describe('', () => {
+ it('should render loading logo', () => {
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ isLoading: true,
+ });
+
+ const { getByTestId } = render();
+ expect(getByTestId(LOADING_INTEGRATIONS_TEST_ID)).toHaveTextContent('Loading integrations');
+ });
+
+ it('should render landing page if no packages are installed', () => {
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ installedPackages: [],
+ isLoading: false,
+ });
+ (useAddIntegrationsUrl as jest.Mock).mockReturnValue({
+ onClick: jest.fn(),
+ });
+
+ const { getByTestId, queryByTestId } = render();
+ expect(queryByTestId(LOADING_INTEGRATIONS_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(LANDING_PAGE_PROMPT_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render wrapper if packages are installed', () => {
+ (useFetchIntegrations as jest.Mock).mockReturnValue({
+ availablePackage: [],
+ installedPackages: [{ id: 'id' }],
+ isLoading: false,
+ });
+
+ const { getByTestId, queryByTestId } = render();
+ expect(queryByTestId(LOADING_INTEGRATIONS_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(LANDING_PAGE_PROMPT_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx
new file mode 100644
index 0000000000000..ed6ad550a17b5
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
+import React, { memo } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations';
+import { LandingPage } from '../../components/alert_summary/landing_page/landing_page';
+import { Wrapper } from '../../components/alert_summary/wrapper';
+
+export const LOADING_INTEGRATIONS_TEST_ID = 'alert-summary-loading-integrations';
+
+const LOADING_INTEGRATIONS = i18n.translate('xpack.securitySolution.alertSummary.loading', {
+ defaultMessage: 'Loading integrations',
+});
+
+/**
+ * Alert summary page rendering alerts generated by AI for SOC integrations.
+ * This page should be only rendered for the AI for SOC product line.
+ */
+export const AlertSummaryPage = memo(() => {
+ const { availablePackage, installedPackages, isLoading } = useFetchIntegrations();
+
+ if (isLoading) {
+ return (
+ }
+ title={{LOADING_INTEGRATIONS}
}
+ />
+ );
+ }
+
+ if (installedPackages.length === 0) {
+ return ;
+ }
+
+ return ;
+});
+
+AlertSummaryPage.displayName = 'AlertSummaryPage';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/index.tsx
new file mode 100644
index 0000000000000..ef5b054d56a61
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/index.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 { Route, Routes } from '@kbn/shared-ux-router';
+import { SecurityRoutePageWrapper } from '../../../common/components/security_route_page_wrapper';
+import { AlertSummaryPage } from './alert_summary';
+import { NotFoundPage } from '../../../app/404';
+import { ALERT_SUMMARY_PATH, SecurityPageName } from '../../../../common/constants';
+import { PluginTemplateWrapper } from '../../../common/components/plugin_template_wrapper';
+
+const AlertSummaryRoute = () => (
+
+
+
+
+
+);
+
+export const AlertSummaryContainer: React.FC = React.memo(() => {
+ return (
+
+
+
+
+ );
+});
+AlertSummaryContainer.displayName = 'AlertSummaryContainer';
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx
index afb2db8aa8e9e..aa5691593fa9a 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx
@@ -6,9 +6,10 @@
*/
import React from 'react';
-import type { RouteProps, RouteComponentProps } from 'react-router-dom';
+import type { RouteComponentProps, RouteProps } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
-import { ALERTS_PATH, DETECTIONS_PATH } from '../../common/constants';
+import { AlertSummaryContainer } from './pages/alert_summary';
+import { ALERT_SUMMARY_PATH, ALERTS_PATH, DETECTIONS_PATH } from '../../common/constants';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { Alerts } from './pages/alerts';
@@ -34,4 +35,8 @@ export const routes: RouteProps[] = [
path: ALERTS_PATH,
component: AlertsRoutes,
},
+ {
+ path: ALERT_SUMMARY_PATH,
+ component: AlertSummaryContainer,
+ },
];