[Cloud Posture] Fully integrate cloud posture pages into security solution (#137058)

This commit is contained in:
Ari Aviran 2022-07-27 14:14:18 +03:00 committed by GitHub
parent 58f7eaf0f8
commit 0be138c0a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 545 additions and 942 deletions

View file

@ -133,7 +133,6 @@ export const applicationUsageSchema = {
// X-Pack
apm: commonSchema,
canvas: commonSchema,
csp: commonSchema,
enterpriseSearch: commonSchema,
enterpriseSearchContent: commonSchema,
elasticsearch: commonSchema,

View file

@ -2092,137 +2092,6 @@
}
}
},
"csp": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"enterpriseSearch": {
"properties": {
"appId": {

View file

@ -36,7 +36,6 @@ export const RULE_FAILED = `failed`;
// A mapping of in-development features to their status. These features should be hidden from users but can be easily
// activated via a simple code change in a single location.
export const INTERNAL_FEATURE_FLAGS = {
showBenchmarks: true,
showManageRulesMock: false,
showFindingsGroupBy: true,
} as const;

View file

@ -1,38 +0,0 @@
/*
* 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 { EuiErrorBoundary } from '@elastic/eui';
import { APP_WRAPPER_CLASS, type AppMountParameters, type CoreStart } from '@kbn/core/public';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider, RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { Router } from 'react-router-dom';
import { cloudPosturePages } from '../common/navigation/constants';
import type { CspClientPluginStartDeps } from '../types';
import { pageToComponentMapping } from './constants';
import { CspRouter, getRoutesFromMapping } from './csp_router';
export interface CspAppDeps {
core: CoreStart;
deps: CspClientPluginStartDeps;
params: AppMountParameters;
}
const cspPluginRoutes = getRoutesFromMapping(cloudPosturePages, pageToComponentMapping);
export const CspApp = ({ core, deps, params }: CspAppDeps) => (
<RedirectAppLinks application={core.application} className={APP_WRAPPER_CLASS}>
<KibanaContextProvider services={{ ...deps, ...core }}>
<EuiErrorBoundary>
<Router history={params.history}>
<I18nProvider>
<CspRouter routes={cspPluginRoutes} />
</I18nProvider>
</Router>
</EuiErrorBoundary>
</KibanaContextProvider>
</RedirectAppLinks>
);

View file

@ -14,10 +14,3 @@ export const pageToComponentMapping: Record<CspPage, RouteProps['component']> =
benchmarks: pages.Benchmarks,
rules: pages.Rules,
};
export const pageToComponentMappingNoPageTemplate: Record<CspPage, RouteProps['component']> = {
findings: pages.FindingsNoPageTemplate,
dashboard: pages.ComplianceDashboardNoPageTemplate,
benchmarks: pages.BenchmarksNoPageTemplate,
rules: pages.RulesNoPageTemplate,
};

View file

@ -11,8 +11,7 @@ import { Redirect, Route, RouteComponentProps, type RouteProps, Switch } from 'r
import { CLOUD_SECURITY_POSTURE_BASE_PATH, type CspSecuritySolutionContext } from '..';
import { cloudPosturePages } from '../common/navigation/constants';
import type { CloudSecurityPosturePageId, CspPageNavigationItem } from '../common/navigation/types';
import { UnknownRoute } from '../components/unknown_route';
import { pageToComponentMappingNoPageTemplate } from './constants';
import { pageToComponentMapping } from './constants';
import { SecuritySolutionContext } from './security_solution_context';
type CspRouteProps = RouteProps & {
@ -60,20 +59,14 @@ export const addSpyRouteComponentToRoute = (
return newRoute;
};
const securitySolutionRoutes = getRoutesFromMapping(
cloudPosturePages,
pageToComponentMappingNoPageTemplate
);
const securitySolutionRoutes = getRoutesFromMapping(cloudPosturePages, pageToComponentMapping);
/** Props for the cloud security posture router component */
export interface CspRouterProps {
routes?: readonly CspRouteProps[];
securitySolutionContext?: CspSecuritySolutionContext;
}
export const CspRouter = ({
routes = securitySolutionRoutes,
securitySolutionContext,
}: CspRouterProps) => {
export const CspRouter = ({ securitySolutionContext }: CspRouterProps) => {
const SpyRoute = securitySolutionContext
? securitySolutionContext.getSpyRouteComponent()
: undefined;
@ -81,12 +74,11 @@ export const CspRouter = ({
const routerElement = (
<QueryClientProvider client={queryClient}>
<Switch>
{routes.map((route) => {
{securitySolutionRoutes.map((route) => {
const routeProps = SpyRoute ? addSpyRouteComponentToRoute(route, SpyRoute) : route;
return <Route key={routeProps.path} {...routeProps} />;
})}
<Route exact path={CLOUD_SECURITY_POSTURE_BASE_PATH} component={RedirectToDashboard} />
<Route path={`${CLOUD_SECURITY_POSTURE_BASE_PATH}/*`} component={UnknownRoute} />
</Switch>
</QueryClientProvider>
);

View file

@ -1,28 +0,0 @@
/*
* 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 ReactDOM from 'react-dom';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import { CspApp } from './app';
import type { CspClientPluginStartDeps } from '../types';
export const renderApp = (
core: CoreStart,
deps: CspClientPluginStartDeps,
params: AppMountParameters
) => {
ReactDOM.render(
<KibanaThemeProvider theme$={params.theme$}>
<CspApp core={core} params={params} deps={deps} />
</KibanaThemeProvider>,
params.element
);
return () => ReactDOM.unmountComponentAtNode(params.element);
};

View file

@ -6,18 +6,17 @@
*/
import { i18n } from '@kbn/i18n';
import { INTERNAL_FEATURE_FLAGS } from '../../../common/constants';
import type { CspPage, CspPageNavigationItem } from './types';
const NAV_ITEMS_NAMES = {
DASHBOARD: i18n.translate('xpack.csp.navigation.dashboardNavItemLabel', {
defaultMessage: 'Dashboard',
defaultMessage: 'Cloud Posture',
}),
FINDINGS: i18n.translate('xpack.csp.navigation.findingsNavItemLabel', {
defaultMessage: 'Findings',
}),
BENCHMARKS: i18n.translate('xpack.csp.navigation.myBenchmarksNavItemLabel', {
defaultMessage: 'My Benchmarks',
defaultMessage: 'CSP Benchmarks',
}),
RULES: i18n.translate('xpack.csp.navigation.rulesNavItemLabel', {
defaultMessage: 'Rules',
@ -41,14 +40,12 @@ export const cloudPosturePages: Record<CspPage, CspPageNavigationItem> = {
rules: {
name: NAV_ITEMS_NAMES.RULES,
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/benchmarks/:packagePolicyId/:policyId/rules`,
disabled: !INTERNAL_FEATURE_FLAGS.showBenchmarks,
id: 'cloud_security_posture-rules',
},
benchmarks: {
name: NAV_ITEMS_NAMES.BENCHMARKS,
path: `${CLOUD_SECURITY_POSTURE_BASE_PATH}/benchmarks`,
exact: true,
disabled: !INTERNAL_FEATURE_FLAGS.showBenchmarks,
id: 'cloud_security_posture-benchmarks',
},
};

View file

@ -6,34 +6,34 @@
*/
import { cloudPosturePages } from './constants';
import { getSecuritySolutionLinks } from './security_solution_links';
import { getSecuritySolutionLink, getSecuritySolutionNavTab } from './security_solution_links';
import { Chance } from 'chance';
import type { CspPage } from './types';
const chance = new Chance();
describe('getSecuritySolutionLinks', () => {
describe('getSecuritySolutionLink', () => {
it('gets the correct link properties', () => {
const cspPage = chance.pickone<CspPage>(['dashboard', 'findings', 'benchmarks', 'rules']);
const links = getSecuritySolutionLinks(cspPage);
const link = getSecuritySolutionLink(cspPage);
expect(links.id).toEqual(cloudPosturePages[cspPage].id);
expect(links.path).toEqual(cloudPosturePages[cspPage].path);
expect(links.title).toEqual(cloudPosturePages[cspPage].name);
});
it('de-structures extensions correctly', () => {
const cspPage = chance.pickone<CspPage>(['dashboard', 'findings', 'benchmarks', 'rules']);
const overwrittenTitle = chance.word();
const extensions = {
[cloudPosturePages[cspPage].id]: { title: overwrittenTitle },
};
const links = getSecuritySolutionLinks(cspPage, extensions);
expect(links.id).toEqual(cloudPosturePages[cspPage].id);
expect(links.path).toEqual(cloudPosturePages[cspPage].path);
expect(links.title).toEqual(overwrittenTitle);
expect(link.id).toEqual(cloudPosturePages[cspPage].id);
expect(link.path).toEqual(cloudPosturePages[cspPage].path);
expect(link.title).toEqual(cloudPosturePages[cspPage].name);
});
});
describe('getSecuritySolutionNavTab', () => {
it('gets the correct nav tab properties', () => {
const cspPage = chance.pickone<CspPage>(['dashboard', 'findings', 'benchmarks', 'rules']);
const basePath = chance.word();
const navTab = getSecuritySolutionNavTab(cspPage, basePath);
expect(navTab.id).toEqual(cloudPosturePages[cspPage].id);
expect(navTab.name).toEqual(cloudPosturePages[cspPage].name);
expect(navTab.href).toEqual(`${basePath}${cloudPosturePages[cspPage].path}`);
expect(navTab.disabled).toEqual(!!cloudPosturePages[cspPage].disabled);
});
});

View file

@ -8,52 +8,42 @@
import { cloudPosturePages } from './constants';
import type { CloudSecurityPosturePageId, CspPage } from './types';
interface BaseLinkItem {
id: string;
interface CloudSecurityPostureLinkItem<TId extends string = CloudSecurityPosturePageId> {
id: TId;
title: string;
path: string;
links?: BaseLinkItem[];
}
type SecuritySolutionLinkExtensions<TLinkItem extends BaseLinkItem> = Partial<
Record<CloudSecurityPosturePageId, Partial<TLinkItem>>
>;
export const getSecuritySolutionLinks = <TLinkItem extends BaseLinkItem = BaseLinkItem>(
cspPage: CspPage,
extensions?: SecuritySolutionLinkExtensions<TLinkItem>
): TLinkItem =>
({
id: cloudPosturePages[cspPage].id,
title: cloudPosturePages[cspPage].name,
path: cloudPosturePages[cspPage].path,
...(extensions?.[cloudPosturePages[cspPage].id] ?? {}),
} as TLinkItem);
interface CloudSecurityPostureNavTab<TId extends string = CloudSecurityPosturePageId> {
id: TId;
name: string;
href: string;
disabled: boolean;
}
/**
* Gets the cloud security posture links for top-level navigation in the security solution.
* @param extensions extended configuration for the links.
* Gets the cloud security posture link properties of a CSP page for navigation in the security solution.
* @param cloudSecurityPosturePage the name of the cloud posture page.
*/
export const getSecuritySolutionRootLinks = <TLinkItem extends BaseLinkItem = BaseLinkItem>(
extensions?: SecuritySolutionLinkExtensions<TLinkItem>
): TLinkItem => getSecuritySolutionLinks('findings', extensions);
export const getSecuritySolutionLink = <TId extends string = CloudSecurityPosturePageId>(
cloudSecurityPosturePage: CspPage
): CloudSecurityPostureLinkItem<TId> => ({
id: cloudPosturePages[cloudSecurityPosturePage].id as TId,
title: cloudPosturePages[cloudSecurityPosturePage].name,
path: cloudPosturePages[cloudSecurityPosturePage].path,
});
/**
* Gets the cloud security posture links for navigation in the security solution's "Dashboards" section.
* @param extensions extended configuration for the links.
* Gets the cloud security posture link properties of a CSP page for navigation in the old security solution navigation.
* @param cloudSecurityPosturePage the name of the cloud posture page.
* @param basePath the base path for links.
*/
export const getSecuritySolutionDashboardLinks = <TLinkItem extends BaseLinkItem = BaseLinkItem>(
extensions?: SecuritySolutionLinkExtensions<TLinkItem>
): TLinkItem => getSecuritySolutionLinks('dashboard', extensions);
/**
* Gets the cloud security posture links for navigation in the security solution's "Manage" section.
* @param extensions extended configuration for the links.
*/
export const getSecuritySolutionManageLinks = <TLinkItem extends BaseLinkItem = BaseLinkItem>(
extensions?: SecuritySolutionLinkExtensions<TLinkItem>
): TLinkItem => {
const manageLinks = getSecuritySolutionLinks('benchmarks', extensions);
manageLinks.links = [getSecuritySolutionLinks('rules', extensions)];
return manageLinks;
};
export const getSecuritySolutionNavTab = <TId extends string = CloudSecurityPosturePageId>(
cloudSecurityPosturePage: CspPage,
basePath: string
): CloudSecurityPostureNavTab<TId> => ({
id: cloudPosturePages[cloudSecurityPosturePage].id as TId,
name: cloudPosturePages[cloudSecurityPosturePage].name,
href: `${basePath}${cloudPosturePages[cloudSecurityPosturePage].path}`,
disabled: !!cloudPosturePages[cloudSecurityPosturePage].disabled,
});

View file

@ -27,6 +27,7 @@ export type CloudSecurityPosturePageId =
| 'cloud_security_posture-benchmarks'
| 'cloud_security_posture-rules';
/** An entry for the cloud security posture breadcrumbs implementation. */
export interface BreadcrumbEntry {
readonly name: string;
readonly path: string;

View file

@ -22,7 +22,7 @@ export const useCISIntegrationLink = (): string | undefined => {
version: cisIntegration.data.item.version,
}),
})
.join('/');
.join('');
return http.basePath.prepend(path);
};

View file

@ -11,6 +11,7 @@ import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
import { css } from '@emotion/react';
import { FullSizeCenteredPage } from './full_size_centered_page';
import { useCspSetupStatusApi } from '../common/api/use_setup_status_api';
import { CspLoadingState } from './csp_loading_state';
import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration';
@ -42,37 +43,39 @@ export const isCommonError = (error: unknown): error is CommonError => {
};
const packageNotInstalledRenderer = (cisIntegrationLink?: string) => (
<NoDataPage
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
css={css`
max-width: 950px;
margin-top: 50px;
margin-left: auto;
margin-right: auto;
`}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle', {
defaultMessage: 'Install Integration to get started',
})}
solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{
elasticAgent: {
href: cisIntegrationLink,
isDisabled: !cisIntegrationLink,
title: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel', {
defaultMessage: 'Add a CIS integration',
}),
description: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.description', {
defaultMessage:
'Use our CIS Kubernetes Benchmark integration to measure your Kubernetes cluster setup against the CIS recommendations.',
}),
},
}}
/>
<FullSizeCenteredPage>
<NoDataPage
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
css={css`
max-width: 950px;
`}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle', {
defaultMessage: 'Install Integration to get started',
})}
solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{
elasticAgent: {
href: cisIntegrationLink,
isDisabled: !cisIntegrationLink,
title: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel', {
defaultMessage: 'Add a CIS integration',
}),
description: i18n.translate(
'xpack.csp.cloudPosturePage.packageNotInstalled.description',
{
defaultMessage:
'Use our CIS Kubernetes Benchmark integration to measure your Kubernetes cluster setup against the CIS recommendations.',
}
),
},
}}
/>
</FullSizeCenteredPage>
);
const defaultLoadingRenderer = () => (
@ -85,57 +88,58 @@ const defaultLoadingRenderer = () => (
);
const defaultErrorRenderer = (error: unknown) => (
<EuiEmptyPrompt
css={css`
margin-top: 50px;
`}
color="danger"
iconType="alert"
data-test-subj={ERROR_STATE_TEST_SUBJECT}
title={
<h2>
<FormattedMessage
id="xpack.csp.cloudPosturePage.errorRenderer.errorTitle"
defaultMessage="We couldn't fetch your cloud security posture data"
/>
</h2>
}
body={
isCommonError(error) ? (
<p>
<FullSizeCenteredPage>
<EuiEmptyPrompt
color="danger"
iconType="alert"
data-test-subj={ERROR_STATE_TEST_SUBJECT}
title={
<h2>
<FormattedMessage
id="xpack.csp.cloudPosturePage.errorRenderer.errorDescription"
defaultMessage="{error} {statusCode}: {body}"
values={{
error: error.body.error,
statusCode: error.body.statusCode,
body: error.body.message,
}}
id="xpack.csp.cloudPosturePage.errorRenderer.errorTitle"
defaultMessage="We couldn't fetch your cloud security posture data"
/>
</p>
) : undefined
}
/>
</h2>
}
body={
isCommonError(error) ? (
<p>
<FormattedMessage
id="xpack.csp.cloudPosturePage.errorRenderer.errorDescription"
defaultMessage="{error} {statusCode}: {body}"
values={{
error: error.body.error,
statusCode: error.body.statusCode,
body: error.body.message,
}}
/>
</p>
) : undefined
}
/>
</FullSizeCenteredPage>
);
const defaultNoDataRenderer = () => {
return (
<NoDataPage
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
css={css`
margin-top: 50px;
`}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.pageTitle', {
defaultMessage: 'No data found',
})}
solution={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel', {
defaultMessage: 'Cloud Security Posture',
})}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{}}
/>
<FullSizeCenteredPage>
<NoDataPage
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.pageTitle', {
defaultMessage: 'No data found',
})}
solution={i18n.translate(
'xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel',
{
defaultMessage: 'Cloud Security Posture',
}
)}
// TODO: Add real docs link once we have it
docsLink={'https://www.elastic.co/guide/index.html'}
logo={'logoSecurity'}
actions={{}}
/>
</FullSizeCenteredPage>
);
};

View file

@ -1,43 +0,0 @@
/*
* 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 { EuiBadge } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Score } from '../../common/types';
interface Props {
value: Score;
}
export const CspHealthBadge = ({ value }: Props) => {
if (value <= 65) {
return (
<EuiBadge color="danger">
<FormattedMessage id="xpack.csp.cspHealthBadge.criticalLabel" defaultMessage="Critical" />
</EuiBadge>
);
}
if (value <= 86) {
return (
<EuiBadge color="warning">
<FormattedMessage id="xpack.csp.cspHealthBadge.warningLabel" defaultMessage="Warning" />
</EuiBadge>
);
}
if (value <= 100) {
return (
<EuiBadge color="success">
<FormattedMessage id="xpack.csp.cspHealthBadge.healthyLabel" defaultMessage="Healthy" />
</EuiBadge>
);
}
return null;
};

View file

@ -5,29 +5,20 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, useEuiTheme } from '@elastic/eui';
import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { css } from '@emotion/react';
import { FullSizeCenteredPage } from './full_size_centered_page';
// Keep this component lean as it is part of the main app bundle
export const CspLoadingState: React.FunctionComponent<{ ['data-test-subj']?: string }> = ({
children,
...rest
}) => {
const { euiTheme } = useEuiTheme();
return (
<EuiFlexGroup
css={css`
padding: ${euiTheme.size.l};
margin-top: 50px;
`}
direction="column"
alignItems="center"
data-test-subj={rest['data-test-subj']}
>
<EuiFlexItem>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
<EuiFlexItem>{children}</EuiFlexItem>
</EuiFlexGroup>
<FullSizeCenteredPage data-test-subj={rest['data-test-subj']}>
<EuiLoadingSpinner size="xl" />
<EuiSpacer />
{children}
</FullSizeCenteredPage>
);
};

View file

@ -1,33 +0,0 @@
/*
* 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 Chance from 'chance';
import { createPageNavigationItemFixture } from '../test/fixtures/navigation_item';
import { getSideNavItems } from './csp_page_template';
const chance = new Chance();
describe('getSideNavItems', () => {
it('maps navigation items to side navigation items', () => {
const navigationItem = createPageNavigationItemFixture();
const id = chance.word();
const sideNavItems = getSideNavItems({ [id]: navigationItem });
expect(sideNavItems).toHaveLength(1);
expect(sideNavItems[0]).toMatchObject({
id,
name: navigationItem.name,
renderItem: expect.any(Function),
});
});
it('does not map disabled navigation items to side navigation items', () => {
const navigationItem = createPageNavigationItemFixture({ disabled: true });
const id = chance.word();
const sideNavItems = getSideNavItems({ [id]: navigationItem });
expect(sideNavItems).toHaveLength(0);
});
});

View file

@ -1,55 +0,0 @@
/*
* 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 { NavLink } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { EuiErrorBoundary } from '@elastic/eui';
import { KibanaPageTemplate, type KibanaPageTemplateProps } from '@kbn/shared-ux-components';
import { cloudPosturePages } from '../common/navigation/constants';
import type { CspNavigationItem } from '../common/navigation/types';
const activeItemStyle = { fontWeight: 700 };
export const getSideNavItems = (
navigationItems: Record<string, CspNavigationItem>
): NonNullable<NonNullable<KibanaPageTemplateProps['solutionNav']>['items']> =>
Object.entries(navigationItems)
.filter(([_, navigationItem]) => !navigationItem.disabled)
.map(([id, navigationItem]) => ({
id,
name: navigationItem.name,
renderItem: () => (
<NavLink to={navigationItem.path} activeStyle={activeItemStyle}>
{navigationItem.name}
</NavLink>
),
}));
const DEFAULT_PAGE_PROPS: KibanaPageTemplateProps = {
solutionNav: {
name: i18n.translate('xpack.csp.cspPageTemplate.navigationTitle', {
defaultMessage: 'Cloud Security Posture',
}),
items: getSideNavItems({
dashboard: cloudPosturePages.dashboard,
findings: cloudPosturePages.findings,
benchmark: cloudPosturePages.benchmarks,
}),
},
restrictWidth: false,
};
export const CspPageTemplate = <TData, TError>({
children,
...kibanaPageTemplateProps
}: KibanaPageTemplateProps) => {
return (
<KibanaPageTemplate {...DEFAULT_PAGE_PROPS} {...kibanaPageTemplateProps}>
<EuiErrorBoundary>{children}</EuiErrorBoundary>
</KibanaPageTemplate>
);
};

View file

@ -0,0 +1,28 @@
/*
* 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 { EuiFlexGroup, type CommonProps } from '@elastic/eui';
import { css } from '@emotion/react';
import React from 'react';
// Keep this component lean as it is part of the main app bundle
export const FullSizeCenteredPage = ({
children,
...rest
}: { children: React.ReactNode } & CommonProps) => (
<EuiFlexGroup
css={css`
// 140px is roughly the Kibana chrome with a bit of space to spare
min-height: calc(100vh - 140px);
`}
justifyContent="center"
alignItems="center"
direction="column"
{...rest}
>
{children}
</EuiFlexGroup>
);

View file

@ -8,6 +8,7 @@
import React from 'react';
import { EuiLoadingLogo, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { FullSizeCenteredPage } from './full_size_centered_page';
import { useCspBenchmarkIntegrations } from '../pages/benchmarks/use_csp_benchmark_integrations';
import { useCISIntegrationPoliciesLink } from '../common/navigation/use_navigate_to_cis_integration_policies';
import { NO_FINDINGS_STATUS_TEST_SUBJ } from './test_subjects';
@ -131,5 +132,9 @@ export const NoFindingsStates = () => {
if (status === 'index-timeout') return <IndexTimeout />; // agent added, index timeout has passed
};
return <CloudPosturePage query={getSetupStatus}>{render()}</CloudPosturePage>;
return (
<CloudPosturePage query={getSetupStatus}>
<FullSizeCenteredPage>{render()}</FullSizeCenteredPage>
</CloudPosturePage>
);
};

View file

@ -1,30 +0,0 @@
/*
* 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 { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CspPageTemplate } from './csp_page_template';
// TODO: Remove when CSP is rendered exclusively under the security solution
export const UnknownRoute = React.memo(() => (
<CspPageTemplate template="centeredContent">
<EuiEmptyPrompt
data-test-subj="unknownRoute"
iconColor="default"
iconType="logoElastic"
title={
<p>
<FormattedMessage
id="xpack.csp.unknownRoute.pageNotFoundTitle"
defaultMessage="Page not found"
/>
</p>
}
/>
</CspPageTemplate>
));

View file

@ -10,9 +10,8 @@ export type { CspSecuritySolutionContext } from './types';
export { CLOUD_SECURITY_POSTURE_BASE_PATH } from './common/navigation/constants';
export type { CloudSecurityPosturePageId } from './common/navigation/types';
export {
getSecuritySolutionRootLinks,
getSecuritySolutionDashboardLinks,
getSecuritySolutionManageLinks,
getSecuritySolutionLink,
getSecuritySolutionNavTab,
} from './common/navigation/security_solution_links';
export type { CspClientPluginSetup, CspClientPluginStart } from './types';

View file

@ -21,10 +21,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { cloudPosturePages } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
import { CspPageTemplate } from '../../components/csp_page_template';
import { BenchmarksTable } from './benchmarks_table';
import {
useCspBenchmarkIntegrations,
@ -33,7 +30,6 @@ import {
import { extractErrorMessage } from '../../../common/utils/helpers';
import * as TEST_SUBJ from './test_subjects';
const BENCHMARKS_BREADCRUMBS = [cloudPosturePages.benchmarks];
const SEARCH_DEBOUNCE_MS = 300;
const AddCisIntegrationButton = () => {
@ -128,7 +124,7 @@ const BenchmarkSearchField = ({
);
};
export const BenchmarksNoPageTemplate = () => {
export const Benchmarks = () => {
const [query, setQuery] = useState<UseCspBenchmarkIntegrationsProps>({
name: '',
page: 1,
@ -194,13 +190,3 @@ export const BenchmarksNoPageTemplate = () => {
</CloudPosturePage>
);
};
export const Benchmarks = () => {
useCspBreadcrumbs(BENCHMARKS_BREADCRUMBS);
return (
<CspPageTemplate>
<BenchmarksNoPageTemplate />
</CspPageTemplate>
);
};

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { Benchmarks, BenchmarksNoPageTemplate } from './benchmarks';
export { Benchmarks } from './benchmarks';

View file

@ -9,18 +9,15 @@ import React from 'react';
import { EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { cloudPosturePages } from '../../common/navigation/constants';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { DASHBOARD_CONTAINER } from './test_subjects';
import { SummarySection } from './dashboard_sections/summary_section';
import { BenchmarksSection } from './dashboard_sections/benchmarks_section';
import { useComplianceDashboardDataApi } from '../../common/api';
import { CspPageTemplate } from '../../components/csp_page_template';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { NoFindingsStates } from '../../components/no_findings_states';
export const ComplianceDashboardNoPageTemplate = () => {
export const ComplianceDashboard = () => {
const getSetupStatus = useCspSetupStatusApi();
const hasFindings = getSetupStatus.data?.status === 'indexed';
const getDashboardData = useComplianceDashboardDataApi({
@ -54,15 +51,3 @@ export const ComplianceDashboardNoPageTemplate = () => {
</CloudPosturePage>
);
};
const COMPLIANCE_DASHBOARD_BREADCRUMBS = [cloudPosturePages.dashboard];
export const ComplianceDashboard = () => {
useCspBreadcrumbs(COMPLIANCE_DASHBOARD_BREADCRUMBS);
return (
<CspPageTemplate>
<ComplianceDashboardNoPageTemplate />
</CspPageTemplate>
);
};

View file

@ -9,17 +9,15 @@ import type { UseQueryResult } from 'react-query';
import { Redirect, Switch, Route, useLocation } from 'react-router-dom';
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
import { NoFindingsStates } from '../../components/no_findings_states';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { useFindingsEsPit } from './es_pit/use_findings_es_pit';
import { FindingsEsPitContext } from './es_pit/findings_es_pit_context';
import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_data_view';
import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants';
import { CspPageTemplate } from '../../components/csp_page_template';
import { FindingsByResourceContainer } from './latest_findings_by_resource/findings_by_resource_container';
import { LatestFindingsContainer } from './latest_findings/latest_findings_container';
export const FindingsNoPageTemplate = () => {
export const Findings = () => {
const location = useLocation();
const dataViewQuery = useLatestFindingsDataView();
// TODO: Consider splitting the PIT window so that each "group by" view has its own PIT
@ -74,15 +72,3 @@ export const FindingsNoPageTemplate = () => {
</CloudPosturePage>
);
};
const FINDINGS_BREADCRUMBS = [cloudPosturePages.findings];
export const Findings = () => {
useCspBreadcrumbs(FINDINGS_BREADCRUMBS);
return (
<CspPageTemplate paddingSize="none">
<FindingsNoPageTemplate />
</CspPageTemplate>
);
};

View file

@ -24,7 +24,7 @@ import {
useBaseEsQuery,
usePersistedQuery,
} from '../utils';
import { PageWrapper, PageTitle, PageTitleText } from '../layout/findings_layout';
import { PageTitle, PageTitleText } from '../layout/findings_layout';
import { FindingsGroupBySelector } from '../layout/findings_group_by_selector';
import { useUrlQuery } from '../../../common/hooks/use_url_query';
import { ErrorCallout } from '../layout/error_callout';
@ -74,64 +74,62 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => {
}}
loading={findingsGroupByNone.isFetching}
/>
<PageWrapper>
<LatestFindingsPageTitle />
{error && <ErrorCallout error={error} />}
{!error && (
<>
<FindingsGroupBySelector type="default" />
{findingsGroupByNone.isSuccess && !!findingsGroupByNone.data.page.length && (
<FindingsDistributionBar
{...{
type: i18n.translate('xpack.csp.findings.latestFindings.tableRowTypeLabel', {
defaultMessage: 'Findings',
}),
total: findingsGroupByNone.data.total,
passed: findingsGroupByNone.data.count.passed,
failed: findingsGroupByNone.data.count.failed,
...getFindingsPageSizeInfo({
pageIndex: urlQuery.pageIndex,
pageSize: urlQuery.pageSize,
currentPageSize: findingsGroupByNone.data.page.length,
}),
}}
/>
)}
<EuiSpacer />
<FindingsTable
loading={findingsGroupByNone.isFetching}
items={findingsGroupByNone.data?.page || []}
pagination={getPaginationTableParams({
pageSize: urlQuery.pageSize,
pageIndex: urlQuery.pageIndex,
totalItemCount: findingsGroupByNone.data?.total || 0,
})}
sorting={{
sort: { field: urlQuery.sort.field, direction: urlQuery.sort.direction },
<LatestFindingsPageTitle />
{error && <ErrorCallout error={error} />}
{!error && (
<>
<FindingsGroupBySelector type="default" />
{findingsGroupByNone.isSuccess && !!findingsGroupByNone.data.page.length && (
<FindingsDistributionBar
{...{
type: i18n.translate('xpack.csp.findings.latestFindings.tableRowTypeLabel', {
defaultMessage: 'Findings',
}),
total: findingsGroupByNone.data.total,
passed: findingsGroupByNone.data.count.passed,
failed: findingsGroupByNone.data.count.failed,
...getFindingsPageSizeInfo({
pageIndex: urlQuery.pageIndex,
pageSize: urlQuery.pageSize,
currentPageSize: findingsGroupByNone.data.page.length,
}),
}}
setTableOptions={({ page, sort }) =>
setUrlQuery({
sort,
pageIndex: page.index,
pageSize: page.size,
})
}
onAddFilter={(field, value, negate) =>
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field,
value,
negate,
}),
})
}
/>
</>
)}
</PageWrapper>
)}
<EuiSpacer />
<FindingsTable
loading={findingsGroupByNone.isFetching}
items={findingsGroupByNone.data?.page || []}
pagination={getPaginationTableParams({
pageSize: urlQuery.pageSize,
pageIndex: urlQuery.pageIndex,
totalItemCount: findingsGroupByNone.data?.total || 0,
})}
sorting={{
sort: { field: urlQuery.sort.field, direction: urlQuery.sort.direction },
}}
setTableOptions={({ page, sort }) =>
setUrlQuery({
sort,
pageIndex: page.index,
pageSize: page.size,
})
}
onAddFilter={(field, value, negate) =>
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field,
value,
negate,
}),
})
}
/>
</>
)}
</div>
);
};

View file

@ -23,7 +23,7 @@ import {
useBaseEsQuery,
usePersistedQuery,
} from '../utils';
import { PageTitle, PageTitleText, PageWrapper } from '../layout/findings_layout';
import { PageTitle, PageTitleText } from '../layout/findings_layout';
import { FindingsGroupBySelector } from '../layout/findings_group_by_selector';
import { findingsNavigation } from '../../../common/navigation/constants';
import { ResourceFindings } from './resource_findings/resource_findings_container';
@ -89,73 +89,71 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => {
}}
loading={findingsGroupByResource.isFetching}
/>
<PageWrapper>
<PageTitle>
<PageTitleText
title={
<FormattedMessage
id="xpack.csp.findings.findingsByResource.findingsByResourcePageTitle"
defaultMessage="Findings"
/>
<PageTitle>
<PageTitleText
title={
<FormattedMessage
id="xpack.csp.findings.findingsByResource.findingsByResourcePageTitle"
defaultMessage="Findings"
/>
}
/>
</PageTitle>
{error && <ErrorCallout error={error} />}
{!error && (
<>
<FindingsGroupBySelector type="resource" />
{findingsGroupByResource.isSuccess && !!findingsGroupByResource.data.page.length && (
<FindingsDistributionBar
{...{
type: i18n.translate('xpack.csp.findings.findingsByResource.tableRowTypeLabel', {
defaultMessage: 'Resources',
}),
total: findingsGroupByResource.data.total,
passed: findingsGroupByResource.data.count.passed,
failed: findingsGroupByResource.data.count.failed,
...getFindingsPageSizeInfo({
pageIndex: urlQuery.pageIndex,
pageSize: urlQuery.pageSize,
currentPageSize: findingsGroupByResource.data.page.length,
}),
}}
/>
)}
<EuiSpacer />
<FindingsByResourceTable
loading={findingsGroupByResource.isFetching}
items={findingsGroupByResource.data?.page || []}
pagination={getPaginationTableParams({
pageSize: urlQuery.pageSize,
pageIndex: urlQuery.pageIndex,
totalItemCount: findingsGroupByResource.data?.total || 0,
})}
setTableOptions={({ sort, page }) =>
setUrlQuery({
sortDirection: sort?.direction,
pageIndex: page.index,
pageSize: page.size,
})
}
sorting={{
sort: { field: 'failed_findings', direction: urlQuery.sortDirection },
}}
onAddFilter={(field, value, negate) =>
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field,
value,
negate,
}),
})
}
/>
</PageTitle>
{error && <ErrorCallout error={error} />}
{!error && (
<>
<FindingsGroupBySelector type="resource" />
{findingsGroupByResource.isSuccess && !!findingsGroupByResource.data.page.length && (
<FindingsDistributionBar
{...{
type: i18n.translate('xpack.csp.findings.findingsByResource.tableRowTypeLabel', {
defaultMessage: 'Resources',
}),
total: findingsGroupByResource.data.total,
passed: findingsGroupByResource.data.count.passed,
failed: findingsGroupByResource.data.count.failed,
...getFindingsPageSizeInfo({
pageIndex: urlQuery.pageIndex,
pageSize: urlQuery.pageSize,
currentPageSize: findingsGroupByResource.data.page.length,
}),
}}
/>
)}
<EuiSpacer />
<FindingsByResourceTable
loading={findingsGroupByResource.isFetching}
items={findingsGroupByResource.data?.page || []}
pagination={getPaginationTableParams({
pageSize: urlQuery.pageSize,
pageIndex: urlQuery.pageIndex,
totalItemCount: findingsGroupByResource.data?.total || 0,
})}
setTableOptions={({ sort, page }) =>
setUrlQuery({
sortDirection: sort?.direction,
pageIndex: page.index,
pageSize: page.size,
})
}
sorting={{
sort: { field: 'failed_findings', direction: urlQuery.sortDirection },
}}
onAddFilter={(field, value, negate) =>
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field,
value,
negate,
}),
})
}
/>
</>
)}
</PageWrapper>
</>
)}
</div>
);
};

View file

@ -12,7 +12,7 @@ import { useEuiTheme } from '@elastic/eui';
import { generatePath } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import * as TEST_SUBJECTS from '../../test_subjects';
import { PageWrapper, PageTitle, PageTitleText } from '../../layout/findings_layout';
import { PageTitle, PageTitleText } from '../../layout/findings_layout';
import { findingsNavigation } from '../../../../common/navigation/constants';
import { ResourceFindingsQuery, useResourceFindings } from './use_resource_findings';
import { useUrlQuery } from '../../../../common/hooks/use_url_query';
@ -93,73 +93,71 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => {
}}
loading={resourceFindings.isFetching}
/>
<PageWrapper>
<PageTitle>
<BackToResourcesButton />
<PageTitleText
title={
<div style={{ padding: euiTheme.size.s }}>
<FormattedMessage
id="xpack.csp.findings.resourceFindings.resourceFindingsPageTitle"
defaultMessage="{resourceId} - Findings"
values={{ resourceId: params.resourceId }}
/>
</div>
<PageTitle>
<BackToResourcesButton />
<PageTitleText
title={
<div style={{ padding: euiTheme.size.s }}>
<FormattedMessage
id="xpack.csp.findings.resourceFindings.resourceFindingsPageTitle"
defaultMessage="{resourceId} - Findings"
values={{ resourceId: params.resourceId }}
/>
</div>
}
/>
</PageTitle>
<EuiSpacer />
{error && <ErrorCallout error={error} />}
{!error && (
<>
{resourceFindings.isSuccess && !!resourceFindings.data.page.length && (
<FindingsDistributionBar
{...{
type: i18n.translate('xpack.csp.findings.resourceFindings.tableRowTypeLabel', {
defaultMessage: 'Findings',
}),
total: resourceFindings.data.total,
passed: resourceFindings.data.count.passed,
failed: resourceFindings.data.count.failed,
...getFindingsPageSizeInfo({
pageIndex: urlQuery.pageIndex,
pageSize: urlQuery.pageSize,
currentPageSize: resourceFindings.data.page.length,
}),
}}
/>
)}
<EuiSpacer />
<ResourceFindingsTable
loading={resourceFindings.isFetching}
items={resourceFindings.data?.page || []}
pagination={getPaginationTableParams({
pageSize: urlQuery.pageSize,
pageIndex: urlQuery.pageIndex,
totalItemCount: resourceFindings.data?.total || 0,
})}
sorting={{
sort: { field: urlQuery.sort.field, direction: urlQuery.sort.direction },
}}
setTableOptions={({ page, sort }) =>
setUrlQuery({ pageIndex: page.index, pageSize: page.size, sort })
}
onAddFilter={(field, value, negate) =>
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field,
value,
negate,
}),
})
}
/>
</PageTitle>
<EuiSpacer />
{error && <ErrorCallout error={error} />}
{!error && (
<>
{resourceFindings.isSuccess && !!resourceFindings.data.page.length && (
<FindingsDistributionBar
{...{
type: i18n.translate('xpack.csp.findings.resourceFindings.tableRowTypeLabel', {
defaultMessage: 'Findings',
}),
total: resourceFindings.data.total,
passed: resourceFindings.data.count.passed,
failed: resourceFindings.data.count.failed,
...getFindingsPageSizeInfo({
pageIndex: urlQuery.pageIndex,
pageSize: urlQuery.pageSize,
currentPageSize: resourceFindings.data.page.length,
}),
}}
/>
)}
<EuiSpacer />
<ResourceFindingsTable
loading={resourceFindings.isFetching}
items={resourceFindings.data?.page || []}
pagination={getPaginationTableParams({
pageSize: urlQuery.pageSize,
pageIndex: urlQuery.pageIndex,
totalItemCount: resourceFindings.data?.total || 0,
})}
sorting={{
sort: { field: urlQuery.sort.field, direction: urlQuery.sort.direction },
}}
setTableOptions={({ page, sort }) =>
setUrlQuery({ pageIndex: page.index, pageSize: page.size, sort })
}
onAddFilter={(field, value, negate) =>
setUrlQuery({
pageIndex: 0,
filters: getFilters({
filters: urlQuery.filters,
dataView,
field,
value,
negate,
}),
})
}
/>
</>
)}
</PageWrapper>
</>
)}
</div>
);
};

View file

@ -13,7 +13,6 @@ import {
EuiTitle,
EuiToolTip,
PropsOf,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import moment from 'moment';
@ -27,20 +26,6 @@ import {
FINDINGS_TABLE_CELL_ADD_NEGATED_FILTER,
} from '../test_subjects';
// TODO: Remove when CSP is rendered exclusively under the security solution
export const PageWrapper: React.FC = ({ children }) => {
const { euiTheme } = useEuiTheme();
return (
<div
css={css`
padding: ${euiTheme.size.l};
`}
>
{children}
</div>
);
};
export const PageTitle: React.FC = ({ children }) => (
<EuiTitle size="l">
<div>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { Findings, FindingsNoPageTemplate } from './findings';
export { Findings } from './findings';
export * from './compliance_dashboard';
export { Benchmarks, BenchmarksNoPageTemplate } from './benchmarks';
export { Rules, RulesNoPageTemplate } from './rules';
export { Benchmarks } from './benchmarks';
export { Rules } from './rules';

View file

@ -15,7 +15,6 @@ import { RulesContainer, type PageUrlParams } from './rules_container';
import { cloudPosturePages } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { useCspIntegrationInfo } from './use_csp_integration';
import { CspPageTemplate } from '../../components/csp_page_template';
import { useKibana } from '../../common/hooks/use_kibana';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { SecuritySolutionContext } from '../../application/security_solution_context';
@ -40,7 +39,7 @@ const getRulesBreadcrumbs = (
return breadCrumbs;
};
export const RulesNoPageTemplate = ({ match: { params } }: RouteComponentProps<PageUrlParams>) => {
export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>) => {
const { http } = useKibana().services;
const integrationInfo = useCspIntegrationInfo(params);
const securitySolutionContext = useContext(SecuritySolutionContext);
@ -112,11 +111,3 @@ export const RulesNoPageTemplate = ({ match: { params } }: RouteComponentProps<P
</CloudPosturePage>
);
};
export const Rules = (props: RouteComponentProps<PageUrlParams>) => {
return (
<CspPageTemplate>
<RulesNoPageTemplate {...props} />
</CspPageTemplate>
);
};

View file

@ -5,35 +5,21 @@
* 2.0.
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { css } from '@emotion/react';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { CspLoadingState } from './components/csp_loading_state';
import type { CspRouterProps } from './application/csp_router';
import { CLOUD_SECURITY_POSTURE_BASE_PATH } from './common/navigation/constants';
import type {
CspClientPluginSetup,
CspClientPluginStart,
CspClientPluginSetupDeps,
CspClientPluginStartDeps,
} from './types';
import { PLUGIN_NAME, PLUGIN_ID } from '../common';
const CspRouterLazy = lazy(() => import('./application/csp_router'));
const CspRouter = (props: CspRouterProps) => (
<Suspense
fallback={
<EuiLoadingSpinner
size="xl"
css={css`
margin-top: 50px;
margin-left: auto;
margin-right: auto;
`}
/>
}
>
<Suspense fallback={<CspLoadingState />}>
<CspRouterLazy {...props} />
</Suspense>
);
@ -51,23 +37,6 @@ export class CspPlugin
core: CoreSetup<CspClientPluginStartDeps, CspClientPluginStart>,
plugins: CspClientPluginSetupDeps
): CspClientPluginSetup {
// Register an application into the side navigation menu
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
euiIconType: 'logoSecurity',
category: DEFAULT_APP_CATEGORIES.security,
defaultPath: CLOUD_SECURITY_POSTURE_BASE_PATH,
async mount(params: AppMountParameters) {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services as specified in kibana.json
const [coreStart, depsStart] = await core.getStartServices();
// Render the application
return renderApp(coreStart, depsStart, params);
},
});
// Return methods that should be available to other plugins
return {};
}
@ -77,7 +46,9 @@ export class CspPlugin
getCloudSecurityPostureRouter: () => (props: CspRouterProps) =>
(
<KibanaContextProvider services={{ ...core, ...plugins }}>
<CspRouter {...props} />
<RedirectAppLinks coreStart={core}>
<CspRouter {...props} />
</RedirectAppLinks>
</KibanaContextProvider>
),
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import React, { useMemo } from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { Router, Switch, Route } from 'react-router-dom';
@ -15,7 +16,13 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
import type { CspAppDeps } from '../application/app';
import type { CspClientPluginStartDeps } from '../types';
interface CspAppDeps {
core: CoreStart;
deps: CspClientPluginStartDeps;
params: AppMountParameters;
}
export const TestProvider: React.FC<Partial<CspAppDeps>> = ({
core = coreMock.createStart(),

View file

@ -10,6 +10,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { CspRouterProps } from './application/csp_router';
import type { BreadcrumbEntry, CloudSecurityPosturePageId } from './common/navigation/types';
/**
@ -23,9 +24,7 @@ export interface CspClientPluginSetup {}
*/
export interface CspClientPluginStart {
/** Gets the cloud security posture router component for embedding in the security solution. */
getCloudSecurityPostureRouter(): ComponentType<{
securitySolutionContext: CspSecuritySolutionContext;
}>;
getCloudSecurityPostureRouter(): ComponentType<CspRouterProps>;
}
export interface CspClientPluginSetupDeps {
@ -53,5 +52,5 @@ export interface CspSecuritySolutionContext {
/** Gets the `SpyRoute` component for navigation highlighting and breadcrumbs. */
getSpyRouteComponent: () => ComponentType<{ pageName?: CloudSecurityPosturePageId }>;
/** Gets the `Manage` breadcrumb entry. */
getManageBreadcrumbEntry: () => BreadcrumbEntry;
getManageBreadcrumbEntry: () => BreadcrumbEntry | undefined;
}

View file

@ -38,11 +38,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
responseActionsConsoleEnabled: true,
/**
* Enables the cloud security posture navigation inside the security solution
*/
cloudSecurityPostureNavigation: false,
/**
* Enables the insights module for related alerts by process ancestry
*/

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public';
import type { LicenseType } from '@kbn/licensing-plugin/common/types';
import { getCasesDeepLinks } from '@kbn/cases-plugin/public';
import {
@ -162,6 +163,10 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
}),
],
},
{
...getSecuritySolutionLink<SecurityPageName>('dashboard'),
features: [FEATURE.general],
},
],
},
{
@ -219,12 +224,18 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
},
],
},
{
...getSecuritySolutionLink<SecurityPageName>('findings'),
features: [FEATURE.general],
navLinkStatus: AppNavLinkStatus.visible,
order: 9002,
},
{
id: SecurityPageName.exploreLanding,
title: EXPLORE,
path: HOSTS_PATH,
navLinkStatus: AppNavLinkStatus.visible,
order: 9004,
order: 9005,
searchable: false,
features: [FEATURE.general],
keywords: [
@ -405,7 +416,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
title: TIMELINES,
path: TIMELINES_PATH,
navLinkStatus: AppNavLinkStatus.visible,
order: 9002,
order: 9003,
features: [FEATURE.general],
keywords: [
i18n.translate('xpack.securitySolution.search.timelines', {
@ -427,7 +438,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
extend: {
[SecurityPageName.case]: {
navLinkStatus: AppNavLinkStatus.visible,
order: 9003,
order: 9004,
features: [FEATURE.casesRead],
},
[SecurityPageName.caseConfigure]: {
@ -447,7 +458,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
path: ENDPOINTS_PATH,
features: [FEATURE.general],
navLinkStatus: AppNavLinkStatus.visible,
order: 9005,
order: 9006,
searchable: false,
keywords: [
i18n.translate('xpack.securitySolution.search.manage', {
@ -486,6 +497,10 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
title: BLOCKLIST,
path: BLOCKLIST_PATH,
},
{
...getSecuritySolutionLink<SecurityPageName>('benchmarks'),
deepLinks: [getSecuritySolutionLink<SecurityPageName>('rules')],
},
{
id: SecurityPageName.responseActions,
title: RESPONSE_ACTIONS,

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { getSecuritySolutionNavTab } from '@kbn/cloud-security-posture-plugin/public';
import * as i18n from '../translations';
import type { SecurityNav, SecurityNavGroup } from '../../common/components/navigation/types';
import { SecurityNavGroupKey } from '../../common/components/navigation/types';
@ -31,6 +32,7 @@ import {
APP_LANDING_PATH,
APP_RESPONSE_ACTIONS_PATH,
APP_THREAT_INTELLIGENCE_PATH,
APP_PATH,
} from '../../../common/constants';
export const navTabs: SecurityNav = {
@ -181,6 +183,22 @@ export const navTabs: SecurityNav = {
disabled: false,
urlKey: 'threat_intelligence',
},
[SecurityPageName.cloudSecurityPostureFindings]: {
...getSecuritySolutionNavTab<SecurityPageName>('findings', APP_PATH),
urlKey: 'findings',
},
[SecurityPageName.cloudSecurityPostureDashboard]: {
...getSecuritySolutionNavTab<SecurityPageName>('dashboard', APP_PATH),
urlKey: 'cloud_posture',
},
[SecurityPageName.cloudSecurityPostureBenchmarks]: {
...getSecuritySolutionNavTab<SecurityPageName>('benchmarks', APP_PATH),
urlKey: 'administration',
},
[SecurityPageName.cloudSecurityPostureRules]: {
...getSecuritySolutionNavTab<SecurityPageName>('rules', APP_PATH),
urlKey: 'administration',
},
};
export const securityNavGroup: SecurityNavGroup = {
@ -192,6 +210,10 @@ export const securityNavGroup: SecurityNavGroup = {
id: SecurityNavGroupKey.detect,
name: i18n.DETECT,
},
[SecurityNavGroupKey.findings]: {
id: SecurityNavGroupKey.findings,
name: i18n.FINDINGS,
},
[SecurityNavGroupKey.explore]: {
id: SecurityNavGroupKey.explore,
name: i18n.EXPLORE,

View file

@ -96,6 +96,9 @@ export const HOST_ISOLATION_EXCEPTIONS = i18n.translate(
export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', {
defaultMessage: 'Detect',
});
export const FINDINGS = i18n.translate('xpack.securitySolution.navigation.findings', {
defaultMessage: 'Findings',
});
export const EXPLORE = i18n.translate('xpack.securitySolution.navigation.explore', {
defaultMessage: 'Explore',
});

View file

@ -4,70 +4,56 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
getSecuritySolutionDashboardLinks,
getSecuritySolutionManageLinks,
getSecuritySolutionRootLinks,
} from '@kbn/cloud-security-posture-plugin/public';
import { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public';
import { i18n } from '@kbn/i18n';
import { SecurityPageName } from '../../common/constants';
import { SecurityPageName, SERVER_APP_ID } from '../../common/constants';
import cloudSecurityPostureDashboardImage from '../common/images/cloud_security_posture_dashboard_page.png';
import type { LinkCategories, LinkItem } from '../common/links/types';
import { IconExceptionLists } from '../management/icons/exception_lists';
const commonLinkProperties: Partial<LinkItem> = {
hideTimeline: true,
experimentalKey: 'cloudSecurityPostureNavigation',
capabilities: [`${SERVER_APP_ID}.show`],
};
export const rootLinks: LinkItem = getSecuritySolutionRootLinks<LinkItem>({
[SecurityPageName.cloudSecurityPostureFindings]: {
globalNavEnabled: true,
globalNavOrder: 3,
...commonLinkProperties,
},
});
export const rootLinks: LinkItem = {
...getSecuritySolutionLink<SecurityPageName>('findings'),
globalNavEnabled: true,
globalNavOrder: 3,
...commonLinkProperties,
};
export const dashboardLinks = getSecuritySolutionDashboardLinks<LinkItem>({
[SecurityPageName.cloudSecurityPostureDashboard]: {
description: i18n.translate(
'xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription',
{
defaultMessage: 'An overview of findings across all CSP integrations.',
}
),
landingImage: cloudSecurityPostureDashboardImage,
// TODO: When CSP is rendered exclusively in the security solution - remove this and rename the title inside the
// CSP plugin
title: i18n.translate('xpack.securitySolution.appLinks.cloudSecurityPostureDashboard', {
defaultMessage: 'Cloud Posture',
}),
...commonLinkProperties,
},
});
export const dashboardLinks: LinkItem = {
...getSecuritySolutionLink<SecurityPageName>('dashboard'),
description: i18n.translate(
'xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription',
{
defaultMessage: 'An overview of findings across all CSP integrations.',
}
),
landingImage: cloudSecurityPostureDashboardImage,
...commonLinkProperties,
};
export const manageLinks: LinkItem = getSecuritySolutionManageLinks<LinkItem>({
[SecurityPageName.cloudSecurityPostureBenchmarks]: {
// TODO: When CSP is rendered exclusively in the security solution - remove this and rename the title inside the
// CSP plugin
title: i18n.translate('xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarks', {
defaultMessage: 'CSP Benchmarks',
}),
description: i18n.translate(
'xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription',
{
defaultMessage: 'View, enable, and or disable benchmark rules.',
}
),
landingIcon: IconExceptionLists,
...commonLinkProperties,
},
[SecurityPageName.cloudSecurityPostureRules]: {
sideNavDisabled: true,
globalSearchDisabled: true,
...commonLinkProperties,
},
});
export const manageLinks: LinkItem = {
...getSecuritySolutionLink<SecurityPageName>('benchmarks'),
description: i18n.translate(
'xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription',
{
defaultMessage: 'View, enable, and or disable benchmark rules.',
}
),
landingIcon: IconExceptionLists,
...commonLinkProperties,
links: [
{
...getSecuritySolutionLink<SecurityPageName>('rules'),
sideNavDisabled: true,
globalSearchDisabled: true,
...commonLinkProperties,
},
],
};
export const manageCategories: LinkCategories = [
{

View file

@ -12,6 +12,7 @@ import {
type CspSecuritySolutionContext,
} from '@kbn/cloud-security-posture-plugin/public';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { useIsGroupedNavigationEnabled } from '../common/components/navigation/helpers';
import { MANAGE_PATH } from '../../common/constants';
import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types';
import { useKibana } from '../common/lib/kibana';
@ -27,11 +28,13 @@ const CloudPostureSpyRoute = ({ pageName }: { pageName?: CloudSecurityPosturePag
const CloudSecurityPosture = memo(() => {
const { cloudSecurityPosture } = useKibana().services;
const isGroupedNavigationEnabled = useIsGroupedNavigationEnabled();
const CloudSecurityPostureRouter = cloudSecurityPosture.getCloudSecurityPostureRouter();
const securitySolutionContext: CspSecuritySolutionContext = {
getFiltersGlobalComponent: () => FiltersGlobal,
getSpyRouteComponent: () => CloudPostureSpyRoute,
getManageBreadcrumbEntry: () => ({ name: MANAGE, path: MANAGE_PATH }),
getManageBreadcrumbEntry: () =>
isGroupedNavigationEnabled ? { name: MANAGE, path: MANAGE_PATH } : undefined,
};
return (

View file

@ -25,6 +25,7 @@ export interface NavGroupTab {
export enum SecurityNavGroupKey {
dashboards = 'dashboards',
detect = 'detect',
findings = 'findings',
explore = 'explore',
investigate = 'investigate',
manage = 'manage',
@ -46,7 +47,9 @@ export type UrlStateType =
| 'timeline'
| 'explore'
| 'dashboards'
| 'threat_intelligence';
| 'threat_intelligence'
| 'cloud_posture'
| 'findings';
export type SecurityNavGroup = Record<SecurityNavGroupKey, NavGroupTab>;
export interface NavTab {
@ -80,6 +83,10 @@ export const securityNavKeys = [
SecurityPageName.users,
SecurityPageName.kubernetes,
SecurityPageName.threatIntelligence,
SecurityPageName.cloudSecurityPostureDashboard,
SecurityPageName.cloudSecurityPostureFindings,
SecurityPageName.cloudSecurityPostureBenchmarks,
SecurityPageName.cloudSecurityPostureRules,
] as const;
export type SecurityNavKey = typeof securityNavKeys[number];

View file

@ -44,6 +44,16 @@ Object {
"name": "Detection & Response",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/cloud_security_posture-dashboard",
"data-test-subj": "navigation-cloud_security_posture-dashboard",
"disabled": false,
"href": "securitySolutionUI/cloud_security_posture-dashboard",
"id": "cloud_security_posture-dashboard",
"isSelected": false,
"name": "Cloud Posture",
"onClick": [Function],
},
],
"name": "Dashboards",
},
@ -83,6 +93,22 @@ Object {
],
"name": "Detect",
},
Object {
"id": "findings",
"items": Array [
Object {
"data-href": "securitySolutionUI/cloud_security_posture-findings",
"data-test-subj": "navigation-cloud_security_posture-findings",
"disabled": false,
"href": "securitySolutionUI/cloud_security_posture-findings",
"id": "cloud_security_posture-findings",
"isSelected": false,
"name": "Findings",
"onClick": [Function],
},
],
"name": "Findings",
},
Object {
"id": "explore",
"items": Array [
@ -208,6 +234,16 @@ Object {
"name": "Blocklist",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/cloud_security_posture-benchmarks",
"data-test-subj": "navigation-cloud_security_posture-benchmarks",
"disabled": false,
"href": "securitySolutionUI/cloud_security_posture-benchmarks",
"id": "cloud_security_posture-benchmarks",
"isSelected": false,
"name": "CSP Benchmarks",
"onClick": [Function],
},
],
"name": "Manage",
},

View file

@ -100,7 +100,7 @@ describe('useSecuritySolutionNavigation', () => {
{ wrapper: TestProviders }
);
expect(result?.current?.items?.[3].items?.[2].id).toEqual(SecurityPageName.users);
expect(result?.current?.items?.[4].items?.[2].id).toEqual(SecurityPageName.users);
});
// TODO: [kubernetes] remove when no longer experimental
@ -110,7 +110,7 @@ describe('useSecuritySolutionNavigation', () => {
() => useSecuritySolutionNavigation(),
{ wrapper: TestProviders }
);
expect(result?.current?.items?.[1].items?.[2].id).toEqual(SecurityPageName.kubernetes);
expect(result?.current?.items?.[1].items?.[3].id).toEqual(SecurityPageName.kubernetes);
});
it('should omit host isolation exceptions if hook reports false', () => {
@ -138,7 +138,7 @@ describe('useSecuritySolutionNavigation', () => {
{ wrapper: TestProviders }
);
const caseNavItem = (result.current?.items || [])[4].items?.find(
const caseNavItem = (result.current?.items || [])[5].items?.find(
(item) => item['data-test-subj'] === 'navigation-cases'
);
expect(caseNavItem).toMatchInlineSnapshot(`

View file

@ -87,6 +87,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
items: [
navTabs[SecurityPageName.overview],
navTabs[SecurityPageName.detectionAndResponse],
navTabs[SecurityPageName.cloudSecurityPostureDashboard],
...(navTabs[SecurityPageName.kubernetes] != null
? [navTabs[SecurityPageName.kubernetes]]
: []),
@ -100,6 +101,10 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
navTabs[SecurityPageName.exceptions],
],
},
{
...securityNavGroup[SecurityNavGroupKey.findings],
items: [navTabs[SecurityPageName.cloudSecurityPostureFindings]],
},
{
...securityNavGroup[SecurityNavGroupKey.explore],
items: [
@ -128,6 +133,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
? [navTabs[SecurityPageName.hostIsolationExceptions]]
: []),
navTabs[SecurityPageName.blocklist],
navTabs[SecurityPageName.cloudSecurityPostureBenchmarks],
],
},
]

View file

@ -22,6 +22,7 @@ const DEPRECATED_HIDDEN_TIMELINE_ROUTES: readonly string[] = [
'/explore',
'/dashboards',
'/manage',
'/cloud_security_posture*',
];
const isTimelinePathVisible = (

View file

@ -5719,19 +5719,6 @@
},
"attributesPerMap": {
"properties": {
"customIconsCount": {
"properties": {
"min": {
"type": "long"
},
"max": {
"type": "long"
},
"avg": {
"type": "float"
}
}
},
"dataSourcesCount": {
"properties": {
"min": {
@ -5791,6 +5778,19 @@
}
}
}
},
"customIconsCount": {
"properties": {
"min": {
"type": "long"
},
"max": {
"type": "long"
},
"avg": {
"type": "float"
}
}
}
}
}

View file

@ -10384,10 +10384,6 @@
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} other {# intégrations}}",
"xpack.csp.cspEvaluationBadge.failLabel": "Échec",
"xpack.csp.cspEvaluationBadge.passLabel": "Réussite",
"xpack.csp.cspHealthBadge.criticalLabel": "Critique",
"xpack.csp.cspHealthBadge.healthyLabel": "Intègre",
"xpack.csp.cspHealthBadge.warningLabel": "Avertissement",
"xpack.csp.cspPageTemplate.navigationTitle": "Niveau de sécurité du cloud",
"xpack.csp.cspSettings.rules": "Règles de sécurité du CSP - ",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "Section CIS",
"xpack.csp.expandColumnDescriptionLabel": "Développer",
@ -10415,7 +10411,6 @@
"xpack.csp.rules.manageIntegrationButtonLabel": "Gérer l'intégration",
"xpack.csp.rules.selectAllButtonLabel": "Tout sélectionner",
"xpack.csp.rules.tableHeader.lastModificationLabel": "Dernière modification de l'intégration {timeAgo} ",
"xpack.csp.unknownRoute.pageNotFoundTitle": "Page introuvable",
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "Choisir le tableau de bord de destination",
"xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "Ouvrir le tableau de bord dans un nouvel onglet",
"xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange": "Utiliser la plage de dates du tableau de bord d'origine",

View file

@ -10376,10 +10376,6 @@
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}を表示しています",
"xpack.csp.cspEvaluationBadge.failLabel": "失敗",
"xpack.csp.cspEvaluationBadge.passLabel": "合格",
"xpack.csp.cspHealthBadge.criticalLabel": "重大",
"xpack.csp.cspHealthBadge.healthyLabel": "正常",
"xpack.csp.cspHealthBadge.warningLabel": "警告",
"xpack.csp.cspPageTemplate.navigationTitle": "クラウドセキュリティ態勢",
"xpack.csp.cspSettings.rules": "CSPセキュリティルール - ",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CISセクション",
"xpack.csp.expandColumnDescriptionLabel": "拡張",
@ -10407,7 +10403,6 @@
"xpack.csp.rules.manageIntegrationButtonLabel": "統合を管理",
"xpack.csp.rules.selectAllButtonLabel": "すべて選択",
"xpack.csp.rules.tableHeader.lastModificationLabel": "統合{timeAgo}の前回変更日 ",
"xpack.csp.unknownRoute.pageNotFoundTitle": "ページが見つかりません",
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "対象ダッシュボードを選択",
"xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "新しいタブでダッシュボードを開く",
"xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange": "元のダッシュボードから日付範囲を使用",

View file

@ -10390,10 +10390,6 @@
"xpack.csp.benchmarks.totalIntegrationsCountMessage": "正在显示 {pageCount}/{totalCount, plural, other {# 个集成}}",
"xpack.csp.cspEvaluationBadge.failLabel": "失败",
"xpack.csp.cspEvaluationBadge.passLabel": "通过",
"xpack.csp.cspHealthBadge.criticalLabel": "紧急",
"xpack.csp.cspHealthBadge.healthyLabel": "运行正常",
"xpack.csp.cspHealthBadge.warningLabel": "警告",
"xpack.csp.cspPageTemplate.navigationTitle": "云安全态势",
"xpack.csp.cspSettings.rules": "CSP 安全规则 - ",
"xpack.csp.dashboard.risksTable.cisSectionColumnLabel": "CIS 部分",
"xpack.csp.expandColumnDescriptionLabel": "展开",
@ -10421,7 +10417,6 @@
"xpack.csp.rules.manageIntegrationButtonLabel": "管理集成",
"xpack.csp.rules.selectAllButtonLabel": "全选",
"xpack.csp.rules.tableHeader.lastModificationLabel": "上次修改集成 {timeAgo} ",
"xpack.csp.unknownRoute.pageNotFoundTitle": "未找到页面",
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "选择目标仪表板",
"xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "在新选项卡中打开仪表板",
"xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange": "使用源仪表板的日期范围",