mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[AI4DSOC] Alert summary page routing and initialization (#214889)
## Summary This PR is the setting the foundations for the AI for SOC Alert summary page. It has very little UI, instead it focuses on the following: - add routing for the `alert_summary` page - fetches the integrations, filters them to only keep the ones related to AI for SOC, then decides what to render depending on if some AI for SOC packages have been installed or not The PR also makes a small change to the `SecurityRoutePageWrapper` component, to allow us to redirect to the Security Solution HomePage instead of the NoPrivilegesPage. While this might not be a long term solution, it is the easiest path forward. In the future, AI for SOC will most likely be its own plugin (leaving outside of Security Solution) hence this will not be needed anymore. Here's the basic behavior of the Alert summary page: - The `Landing page` will be shown if none of the hardcoded AI for SOC packages are installed (these values are hardcoded as we currently do not have a way to filter integrations for the AI for SOC ones only): - splunk // doesnt yet exist - google_secops - microsoft_sentinel - sentinel_one - crowdstrike - The `Wrapper` component will only be shown if you have at least one of the above AI for SOC packages installed. ### Very limited UI added in this PR | Loading integrations | No installed packages | Some installed packages | | ------------- | ------------- | ------------- | |  |  |  | ### Notes We need to remove the section at the top of the page that currently shows the `Add integrations` button. A follow PR will take care of that. [This](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/public/app/home/index.tsx#L54) is where that bar is being added. We will have to find a way to not show that for the AI for SOC tier. ## How to test This needs to be ran in Serverless: - `yarn es serverless --projectType security` - `yarn serverless-security --no-base-path` You also need to enable the AI for SOC tier, by adding the following to your `serverless.security.dev.yaml` file: ``` xpack.securitySolutionServerless.productTypes: [ { product_line: 'ai_soc', product_tier: 'search_ai_lake' }, ] ``` The Alert summary navigation will NOT be shown for the following Serverless users: `viewer`, `t1_analyst`. and `t2_analyst`. For those, the navigation entry is not present, and navigating to the url directly will automatically re-route to the Security home page. Currently, retrieving the integrations (via the `fleet/epm/packages` endpoint) is also unauthorized for the following users: `editor`, `t3_analyst`, `threat_intelligence_analyst`, `rule_author`, `soc_manager` and `detections_admin`. This means that the only users that can be currently used to test this PR are: - `platform_engineer` - `endpoint_operations_analyst` - `endpoint_policy_manager` - `admin` - `system_indices_superuser` ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Will help close https://github.com/elastic/security-team/issues/11954 as well as https://github.com/elastic/security-team/issues/11979.
This commit is contained in:
parent
108716dce8
commit
3605a331b9
14 changed files with 527 additions and 66 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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(() => <div data-test-subj={REDIRECT_COMPONENT_SUBJ} />);
|
||||
|
@ -47,23 +43,17 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|||
);
|
||||
|
||||
describe('SecurityRoutePageWrapper', () => {
|
||||
it('should render children when authorized', () => {
|
||||
mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo }); // authorized
|
||||
const { getByTestId } = render(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render UpsellPage when unauthorized and UpsellPage is available', () => {
|
||||
const TestUpsellPage = () => <div data-test-subj={'test-upsell-page'} />;
|
||||
it('should render UpsellPage when it is available', () => {
|
||||
const TEST_ID = 'test-upsell-page';
|
||||
const TestUpsellPage = () => <div data-test-subj={TEST_ID} />;
|
||||
|
||||
(useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo);
|
||||
(useUpsellingPage as jest.Mock).mockReturnValue(TestUpsellPage);
|
||||
|
||||
mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
|
||||
mockUseUpsellingPage.mockReturnValue(TestUpsellPage);
|
||||
const { getByTestId } = render(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
|
@ -71,24 +61,12 @@ describe('SecurityRoutePageWrapper', () => {
|
|||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId('test-upsell-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render NoPrivilegesPage when unauthorized and UpsellPage is unavailable', () => {
|
||||
mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
|
||||
mockUseUpsellingPage.mockReturnValue(undefined);
|
||||
const { getByTestId } = render(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
|
||||
expect(getByTestId(TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should redirect when link missing and redirectOnMissing flag present', () => {
|
||||
mockGetLink.mockReturnValueOnce(undefined);
|
||||
(useLinkInfo as jest.Mock).mockReturnValue(undefined);
|
||||
(useUpsellingPage as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectOnMissing>
|
||||
|
@ -99,4 +77,74 @@ describe('SecurityRoutePageWrapper', () => {
|
|||
|
||||
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(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectIfUnauthorized>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectIfUnauthorized>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ 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);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render children when authorized', () => {
|
||||
(useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo);
|
||||
(useUpsellingPage as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
|
||||
<TestComponent />
|
||||
</SecurityRoutePageWrapper>,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<PropsWithChildren<SecurityRoutePageWrapperProps>> = ({
|
||||
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<PropsWithChildren<SecurityRoutePageWra
|
|||
);
|
||||
}
|
||||
|
||||
// Allows a redirect to the home page.
|
||||
if (redirectOnMissing && link == null) {
|
||||
return <Redirect to="" />;
|
||||
}
|
||||
|
||||
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 <Redirect to="" />;
|
||||
}
|
||||
|
||||
// Show the no privileges page if the link is undefined or unauthorized.
|
||||
if (!isAuthorized) {
|
||||
return (
|
||||
<TrackApplicationView viewId={pageName}>
|
||||
{children}
|
||||
<>
|
||||
<SpyRoute pageName={pageName} />
|
||||
</TrackApplicationView>
|
||||
<NoPrivilegesPage
|
||||
pageName={pageName}
|
||||
docLinkSelector={(docLinks) => docLinks.siem.privileges}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (redirectOnMissing && link == null) {
|
||||
return <Redirect to="" />; // redirects to the home page
|
||||
}
|
||||
|
||||
// Show the actual application page.
|
||||
return (
|
||||
<>
|
||||
<TrackApplicationView viewId={pageName}>
|
||||
{children}
|
||||
<SpyRoute pageName={pageName} />
|
||||
<NoPrivilegesPage
|
||||
pageName={pageName}
|
||||
docLinkSelector={(docLinks) => docLinks.siem.privileges}
|
||||
/>
|
||||
</>
|
||||
</TrackApplicationView>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 <EuiText data-test-subj={LANDING_PAGE_PROMPT_TEST_ID}>{'Landing page'}</EuiText>;
|
||||
});
|
||||
|
||||
LandingPage.displayName = 'LandingPage';
|
|
@ -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 <EuiText data-test-subj={DATA_VIEW_LOADING_PROMPT_TEST_ID}>{'Wrapper'}</EuiText>;
|
||||
});
|
||||
|
||||
Wrapper.displayName = 'Wrapper';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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 {
|
||||
ALERTS_PATH,
|
||||
SecurityPageName,
|
||||
SECURITY_FEATURE_ID,
|
||||
ALERT_SUMMARY_PATH,
|
||||
ALERTS_PATH,
|
||||
SECURITY_FEATURE_ID,
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -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('<AlertSummaryPage />', () => {
|
||||
it('should render loading logo', () => {
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<AlertSummaryPage />);
|
||||
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(<AlertSummaryPage />);
|
||||
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(<AlertSummaryPage />);
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj={LOADING_INTEGRATIONS_TEST_ID}
|
||||
icon={<EuiLoadingLogo logo="logoSecurity" size="xl" />}
|
||||
title={<h2>{LOADING_INTEGRATIONS}</h2>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (installedPackages.length === 0) {
|
||||
return <LandingPage packages={availablePackage} />;
|
||||
}
|
||||
|
||||
return <Wrapper packages={installedPackages} />;
|
||||
});
|
||||
|
||||
AlertSummaryPage.displayName = 'AlertSummaryPage';
|
|
@ -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 = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.alertSummary} redirectIfUnauthorized>
|
||||
<AlertSummaryPage />
|
||||
</SecurityRoutePageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
);
|
||||
|
||||
export const AlertSummaryContainer: React.FC = React.memo(() => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={ALERT_SUMMARY_PATH} exact component={AlertSummaryRoute} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Routes>
|
||||
);
|
||||
});
|
||||
AlertSummaryContainer.displayName = 'AlertSummaryContainer';
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue