Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions x-pack/platform/plugins/shared/fleet/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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} />);
Expand All @@ -47,36 +43,87 @@ 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 = () => <div data-test-subj={TEST_ID} />;

(useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo);
(useUpsellingPage as jest.Mock).mockReturnValue(TestUpsellPage);

const { getByTestId } = render(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
</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(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectOnMissing>
<TestComponent />
</SecurityRoutePageWrapper>,
{ 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(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectIfUnauthorized>
<TestComponent />
</SecurityRoutePageWrapper>,
{ wrapper: Wrapper }
);

expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
});

it('should render UpsellPage when unauthorized and UpsellPage is available', () => {
const TestUpsellPage = () => <div data-test-subj={'test-upsell-page'} />;
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);

mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
mockUseUpsellingPage.mockReturnValue(TestUpsellPage);
const { getByTestId } = render(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
</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(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
Expand All @@ -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(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectOnMissing>
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
</SecurityRoutePageWrapper>,
{ wrapper: Wrapper }
);

expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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 (
<>
Expand All @@ -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>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading