[Security Solutions] Add upselling service to security solutions ESS plugin (#163406)

## Summary

* Use Serverless upsell architecture on ESS. But check for the required
license instead of capabilities.
* Covert Investigation Guide and Entity Analytics upsell to the new
architecture.
* Update upsell registering functions always to clear the state when
registering a new value. It fits perfectly ESS because the license is
observable.
### 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/packages/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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2023-08-16 16:20:25 +02:00 committed by GitHub
parent a00c2401e2
commit 5b2859f202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 534 additions and 301 deletions

View file

@ -18,7 +18,6 @@ import React, {
} from 'react';
import { EuiMarkdownEditor } from '@elastic/eui';
import type { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context';
import { useLicense } from '../../hooks/use_license';
import { uiPlugins, parsingPlugins, processingPlugins } from './plugins';
import { useUpsellingMessage } from '../../hooks/use_upselling';
@ -72,12 +71,10 @@ const MarkdownEditorComponent = forwardRef<MarkdownEditorRef, MarkdownEditorProp
}
}, [autoFocusDisabled]);
const licenseIsPlatinum = useLicense().isPlatinumPlus();
const insightsUpsellingMessage = useUpsellingMessage('investigation_guide');
const uiPluginsWithState = useMemo(() => {
return uiPlugins({ licenseIsPlatinum, insightsUpsellingMessage });
}, [licenseIsPlatinum, insightsUpsellingMessage]);
return uiPlugins({ insightsUpsellingMessage });
}, [insightsUpsellingMessage]);
// @ts-expect-error update types
useImperativeHandle(ref, () => {

View file

@ -27,15 +27,12 @@ export const {
export const platinumOnlyPluginTokens = [insightMarkdownPlugin.insightPrefix];
export const uiPlugins = ({
licenseIsPlatinum,
insightsUpsellingMessage,
}: {
licenseIsPlatinum: boolean;
insightsUpsellingMessage: string | null;
}) => {
const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name);
const insightPluginWithLicense = insightMarkdownPlugin.plugin({
licenseIsPlatinum,
insightsUpsellingMessage,
});
if (currentPlugins.includes(insightPluginWithLicense.name) === false) {

View file

@ -134,35 +134,21 @@ describe('insight component renderer', () => {
describe('plugin', () => {
it('renders insightsUpsellingMessage when provided', () => {
const insightsUpsellingMessage = 'test message';
const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage });
const result = plugin({ insightsUpsellingMessage });
expect(result.button.label).toEqual(insightsUpsellingMessage);
});
it('disables the button when insightsUpsellingMessage is provided', () => {
const insightsUpsellingMessage = 'test message';
const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage });
const result = plugin({ insightsUpsellingMessage });
expect(result.button.isDisabled).toBeTruthy();
});
it('disables the button when license is not Platinum', () => {
const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage: null });
expect(result.button.isDisabled).toBeTruthy();
});
it('show investigate message when license is Platinum', () => {
const result = plugin({ licenseIsPlatinum: true, insightsUpsellingMessage: null });
it('show investigate message when insightsUpsellingMessage is not provided', () => {
const result = plugin({ insightsUpsellingMessage: null });
expect(result.button.label).toEqual('Investigate');
});
it('show upsell message when license is not Platinum', () => {
const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage: null });
expect(result.button.label).toEqual(
'Upgrade to platinum to make use of insights in investigation guides'
);
});
});

View file

@ -542,20 +542,16 @@ const exampleInsight = `${insightPrefix}{
}}`;
export const plugin = ({
licenseIsPlatinum,
insightsUpsellingMessage,
}: {
licenseIsPlatinum: boolean;
insightsUpsellingMessage: string | null;
}) => {
const label = licenseIsPlatinum ? i18n.INVESTIGATE : i18n.INSIGHT_UPSELL;
return {
name: 'insights',
button: {
label: insightsUpsellingMessage ?? label,
label: insightsUpsellingMessage ?? i18n.INVESTIGATE,
iconType: 'timelineWithArrow',
isDisabled: !licenseIsPlatinum || !!insightsUpsellingMessage,
isDisabled: !!insightsUpsellingMessage,
},
helpText: (
<div>

View file

@ -11,10 +11,6 @@ export const LABEL = i18n.translate('xpack.securitySolution.markdown.insight.lab
defaultMessage: 'Label',
});
export const INSIGHT_UPSELL = i18n.translate('xpack.securitySolution.markdown.insight.upsell', {
defaultMessage: 'Upgrade to platinum to make use of insights in investigation guides',
});
export const INVESTIGATE = i18n.translate('xpack.securitySolution.markdown.insight.title', {
defaultMessage: 'Investigate',
});

View file

@ -1,94 +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, { memo, useCallback } from 'react';
import {
EuiCard,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiTextColor,
EuiImage,
} from '@elastic/eui';
import styled from 'styled-components';
import { useNavigation } from '../../lib/kibana';
import * as i18n from './translations';
import paywallPng from '../../images/entity_paywall.png';
const PaywallDiv = styled.div`
max-width: 75%;
margin: 0 auto;
.euiCard__betaBadgeWrapper {
.euiCard__betaBadge {
width: auto;
}
}
.platinumCardDescription {
padding: 0 15%;
}
`;
const StyledEuiCard = styled(EuiCard)`
span.euiTitle {
max-width: 540px;
display: block;
margin: 0 auto;
}
`;
export const Paywall = memo(({ heading }: { heading?: string }) => {
const { getAppUrl, navigateTo } = useNavigation();
const subscriptionUrl = getAppUrl({
appId: 'management',
path: 'stack/license_management',
});
const goToSubscription = useCallback(() => {
navigateTo({ url: subscriptionUrl });
}, [navigateTo, subscriptionUrl]);
return (
<PaywallDiv>
<StyledEuiCard
data-test-subj="platinumCard"
betaBadgeProps={{ label: i18n.PLATINUM }}
icon={<EuiIcon size="xl" type="lock" />}
display="subdued"
title={
<h3>
<strong>{heading}</strong>
</h3>
}
description={false}
paddingSize="xl"
>
<EuiFlexGroup className="platinumCardDescription" direction="column" gutterSize="none">
<EuiText>
<EuiFlexItem>
<p>
<EuiTextColor color="subdued">{i18n.UPGRADE_MESSAGE}</EuiTextColor>
</p>
</EuiFlexItem>
<EuiFlexItem>
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON}
</EuiButton>
</div>
</EuiFlexItem>
</EuiText>
</EuiFlexGroup>
</StyledEuiCard>
<EuiFlexGroup>
<EuiFlexItem>
<EuiImage alt={i18n.UPGRADE_MESSAGE} src={paywallPng} size="fullWidth" />
</EuiFlexItem>
</EuiFlexGroup>
</PaywallDiv>
);
});
Paywall.displayName = 'Paywall';

View file

@ -35,7 +35,7 @@ const RenderWrapper: React.FunctionComponent = ({ children }) => {
describe('use_upselling', () => {
test('useUpsellingComponent returns sections', () => {
mockUpselling.registerSections({
mockUpselling.setSections({
entity_analytics_panel: TestComponent,
});
@ -47,7 +47,7 @@ describe('use_upselling', () => {
});
test('useUpsellingPage returns pages', () => {
mockUpselling.registerPages({
mockUpselling.setPages({
[SecurityPageName.hosts]: TestComponent,
});
@ -57,9 +57,9 @@ describe('use_upselling', () => {
expect(result.current).toBe(TestComponent);
});
test('useUpsellingMessage returns pages', () => {
test('useUpsellingMessage returns messages', () => {
const testMessage = 'test message';
mockUpselling.registerMessages({
mockUpselling.setMessages({
investigation_guide: testMessage,
});
@ -72,7 +72,7 @@ describe('use_upselling', () => {
test('useUpsellingMessage returns null when upsellingMessageId not found', () => {
const emptyMessages = {};
mockUpselling.registerMessages(emptyMessages);
mockUpselling.setPages(emptyMessages);
const { result } = renderHook(
() => useUpsellingMessage('my_fake_message_id' as 'investigation_guide'),

View file

@ -14,7 +14,7 @@ const TestComponent = () => <div>{'TEST component'}</div>;
describe('UpsellingService', () => {
it('registers sections', async () => {
const service = new UpsellingService();
service.registerSections({
service.setSections({
entity_analytics_panel: TestComponent,
});
@ -23,9 +23,24 @@ describe('UpsellingService', () => {
expect(value.get('entity_analytics_panel')).toEqual(TestComponent);
});
it('overwrites registered sections when called twice', async () => {
const service = new UpsellingService();
service.setSections({
entity_analytics_panel: TestComponent,
});
service.setSections({
osquery_automated_response_actions: TestComponent,
});
const value = await firstValueFrom(service.sections$);
expect(Array.from(value.keys())).toEqual(['osquery_automated_response_actions']);
});
it('registers pages', async () => {
const service = new UpsellingService();
service.registerPages({
service.setPages({
[SecurityPageName.hosts]: TestComponent,
});
@ -34,10 +49,25 @@ describe('UpsellingService', () => {
expect(value.get(SecurityPageName.hosts)).toEqual(TestComponent);
});
it('overwrites registered pages when called twice', async () => {
const service = new UpsellingService();
service.setPages({
[SecurityPageName.hosts]: TestComponent,
});
service.setPages({
[SecurityPageName.users]: TestComponent,
});
const value = await firstValueFrom(service.pages$);
expect(Array.from(value.keys())).toEqual([SecurityPageName.users]);
});
it('registers messages', async () => {
const testMessage = 'test message';
const service = new UpsellingService();
service.registerMessages({
service.setMessages({
investigation_guide: testMessage,
});
@ -46,9 +76,23 @@ describe('UpsellingService', () => {
expect(value.get('investigation_guide')).toEqual(testMessage);
});
it('overwrites registered messages when called twice', async () => {
const testMessage = 'test message';
const service = new UpsellingService();
service.setMessages({
investigation_guide: testMessage,
});
service.setMessages({});
const value = await firstValueFrom(service.messages$);
expect(Array.from(value.keys())).toEqual([]);
});
it('"isPageUpsellable" returns true when page is upsellable', () => {
const service = new UpsellingService();
service.registerPages({
service.setPages({
[SecurityPageName.hosts]: TestComponent,
});
@ -57,7 +101,7 @@ describe('UpsellingService', () => {
it('"getPageUpselling" returns page component when page is upsellable', () => {
const service = new UpsellingService();
service.registerPages({
service.setPages({
[SecurityPageName.hosts]: TestComponent,
});

View file

@ -43,24 +43,33 @@ export class UpsellingService {
this.messages$ = this.messagesSubject$.asObservable();
}
registerSections(sections: SectionUpsellings) {
setSections(sections: SectionUpsellings) {
this.sections.clear();
Object.entries(sections).forEach(([sectionId, component]) => {
this.sections.set(sectionId as UpsellingSectionId, component);
});
this.sectionsSubject$.next(this.sections);
}
registerPages(pages: PageUpsellings) {
setPages(pages: PageUpsellings) {
this.pages.clear();
Object.entries(pages).forEach(([pageId, component]) => {
this.pages.set(pageId as SecurityPageName, component);
});
this.pagesSubject$.next(this.pages);
}
registerMessages(messages: MessageUpsellings) {
setMessages(messages: MessageUpsellings) {
this.messages.clear();
Object.entries(messages).forEach(([messageId, component]) => {
this.messages.set(messageId as UpsellingMessageId, component);
});
this.messagesSubject$.next(this.messages);
}

View file

@ -182,9 +182,9 @@ describe('Security links', () => {
expect(result.current).toStrictEqual([networkLinkItem]);
});
it('should return unauthorized page when page has upselling', async () => {
it('should return unauthorized page when page has upselling (serverless)', async () => {
const upselling = new UpsellingService();
upselling.registerPages({ [SecurityPageName.network]: () => <span /> });
upselling.setPages({ [SecurityPageName.network]: () => <span /> });
const { result, waitForNextUpdate } = renderUseAppLinks();
const networkLinkItem = {
@ -192,8 +192,6 @@ describe('Security links', () => {
title: 'Network',
path: '/network',
capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${CASES_FEATURE_ID}.write_cases`],
experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults,
hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults,
licenseType: 'basic' as const,
};
@ -249,6 +247,67 @@ describe('Security links', () => {
expect(result.current).toStrictEqual([{ ...networkLinkItem, unauthorized: true }]);
});
it('should return unauthorized page when page has upselling (ESS)', async () => {
const upselling = new UpsellingService();
upselling.setPages({ [SecurityPageName.network]: () => <span /> });
const { result, waitForNextUpdate } = renderUseAppLinks();
const hostLinkItem = {
id: SecurityPageName.hosts,
title: 'Hosts',
path: '/hosts',
licenseType: 'platinum' as const,
};
mockUpselling.setPages({
[SecurityPageName.hosts]: () => <span />,
});
await act(async () => {
updateAppLinks([hostLinkItem], {
capabilities: mockCapabilities,
experimentalFeatures: mockExperimentalDefaults,
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
upselling: mockUpselling,
});
await waitForNextUpdate();
});
expect(result.current).toStrictEqual([{ ...hostLinkItem, unauthorized: true }]);
// cleanup
mockUpselling.setPages({});
});
it('should filter out experimental page even if it has upselling', async () => {
const upselling = new UpsellingService();
upselling.setPages({ [SecurityPageName.network]: () => <span /> });
const { result, waitForNextUpdate } = renderUseAppLinks();
const hostLinkItem = {
id: SecurityPageName.hosts,
title: 'Hosts',
path: '/hosts',
licenseType: 'platinum' as const,
experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults,
};
mockUpselling.setPages({
[SecurityPageName.hosts]: () => <span />,
});
await act(async () => {
updateAppLinks([hostLinkItem], {
capabilities: mockCapabilities,
experimentalFeatures: mockExperimentalDefaults,
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
upselling: mockUpselling,
});
await waitForNextUpdate();
});
expect(result.current).toStrictEqual([]);
// cleanup
mockUpselling.setPages({});
});
});
describe('useLinkExists', () => {

View file

@ -153,10 +153,14 @@ const getNormalizedLink = (id: SecurityPageName): Readonly<NormalizedLink> | und
const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissions): LinkItem[] =>
appLinks.reduce<LinkItem[]>((acc, { links, ...appLinkWithoutSublinks }) => {
if (!isLinkAllowed(appLinkWithoutSublinks, linksPermissions)) {
if (!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions)) {
return acc;
}
if (!hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities)) {
if (
!hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities) ||
!isLinkLicenseAllowed(appLinkWithoutSublinks, linksPermissions)
) {
if (linksPermissions.upselling.isPageUpsellable(appLinkWithoutSublinks.id)) {
acc.push({ ...appLinkWithoutSublinks, unauthorized: true });
}
@ -175,7 +179,21 @@ const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissi
return acc;
}, []);
const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksPermissions) => {
const isLinkExperimentalKeyAllowed = (
link: LinkItem,
{ experimentalFeatures }: LinksPermissions
) => {
if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) {
return false;
}
if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) {
return false;
}
return true;
};
const isLinkLicenseAllowed = (link: LinkItem, { license }: LinksPermissions) => {
const linkLicenseType = link.licenseType ?? 'basic';
if (license) {
if (!license.hasAtLeast(linkLicenseType)) {
@ -184,11 +202,5 @@ const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksP
} else if (linkLicenseType !== 'basic') {
return false;
}
if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) {
return false;
}
if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) {
return false;
}
return true;
};

View file

@ -6,6 +6,7 @@
*/
import type { PluginInitializerContext } from '@kbn/core/public';
import { Plugin } from './plugin';
import type { PluginSetup, PluginStart } from './types';
export type { TimelineModel } from './timelines/store/timeline/model';

View file

@ -166,7 +166,7 @@ describe('Onboarding Component new section', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
beforeEach(() => {
mockedContext.startServices.upselling.registerSections({
mockedContext.startServices.upselling.setSections({
endpointPolicyProtections: () => <div data-test-subj="paywall">{'pay up!'}</div>,
});
newPolicy = getMockNewPackage();

View file

@ -66,7 +66,7 @@ describe('Endpoint Policy Settings Form', () => {
describe('and when policy protections are not available', () => {
beforeEach(() => {
upsellingService.registerSections({
upsellingService.setSections({
endpointPolicyProtections: () => <div data-test-subj="paywall">{'pay up!'}</div>,
});
});

View file

@ -13,10 +13,11 @@ import type { PluginStart, PluginSetup } from './types';
const setupMock = (): PluginSetup => ({
resolver: jest.fn(),
upselling: new UpsellingService(),
setAppLinksSwitcher: jest.fn(),
});
const upselling = new UpsellingService();
const startMock = (): PluginStart => ({
getNavLinks$: jest.fn(() => new BehaviorSubject<NavigationLink[]>([])),
setIsSidebarEnabled: jest.fn(),
@ -25,6 +26,7 @@ const startMock = (): PluginStart => ({
() => new BehaviorSubject<BreadcrumbsNav>({ leading: [], trailing: [] })
),
setExtraRoutes: jest.fn(),
getUpselling: () => upselling,
});
export const securitySolutionMock = {

View file

@ -90,6 +90,7 @@ export const entityAnalyticsLinks: LinkItem = {
path: ENTITY_ANALYTICS_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
isBeta: false,
licenseType: 'platinum',
globalSearchKeywords: [ENTITY_ANALYTICS],
};

View file

@ -10,15 +10,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { EntityAnalyticsRiskScores } from '../components/entity_analytics/risk_score';
import { RiskScoreEntity } from '../../../common/search_strategy';
import { ENTITY_ANALYTICS } from '../../app/translations';
import { Paywall } from '../../common/components/paywall';
import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { SecurityPageName } from '../../app/types';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { HeaderPage } from '../../common/components/header_page';
import { LandingPageComponent } from '../../common/components/landing_page';
import * as i18n from './translations';
import { EntityAnalyticsHeader } from '../components/entity_analytics/header';
import { EntityAnalyticsAnomalies } from '../components/entity_analytics/anomalies';
@ -32,26 +29,20 @@ import { useHasSecurityCapability } from '../../helper_hooks';
const EntityAnalyticsComponent = () => {
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
const { indicesExist, loading: isSourcererLoading, indexPattern } = useSourcererDataView();
const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const isRiskScoreModuleLicenseAvailable =
isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
const isRiskScoreModuleLicenseAvailable = useHasSecurityCapability('entity-analytics');
return (
<>
{indicesExist ? (
<>
{isPlatinumOrTrialLicense && capabilitiesFetched && (
<FiltersGlobal>
<SiemSearchBar id={InputsModelId.global} indexPattern={indexPattern} />
</FiltersGlobal>
)}
<FiltersGlobal>
<SiemSearchBar id={InputsModelId.global} indexPattern={indexPattern} />
</FiltersGlobal>
<SecuritySolutionPageWrapper data-test-subj="entityAnalyticsPage">
<HeaderPage title={ENTITY_ANALYTICS} />
{!isPlatinumOrTrialLicense && capabilitiesFetched ? (
<Paywall heading={i18n.ENTITY_ANALYTICS_LICENSE_DESC} />
) : isSourcererLoading ? (
{isSourcererLoading ? (
<EuiLoadingSpinner size="l" data-test-subj="entityAnalyticsLoader" />
) : (
<EuiFlexGroup direction="column" data-test-subj="entityAnalyticsSections">

View file

@ -104,13 +104,6 @@ export const DETECTION_RESPONSE_TITLE = i18n.translate(
}
);
export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate(
'xpack.securitySolution.entityAnalytics.pageDesc',
{
defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics',
}
);
export const TECHNICAL_PREVIEW = i18n.translate(
'xpack.securitySolution.entityAnalytics.technicalPreviewLabel',
{

View file

@ -40,7 +40,6 @@ export class PluginContract {
public getSetupContract(): PluginSetup {
return {
resolver: lazyResolver,
upselling: this.upsellingService,
setAppLinksSwitcher: (appLinksSwitcher) => {
this.appLinksSwitcher = appLinksSwitcher;
},
@ -57,6 +56,7 @@ export class PluginContract {
this.getStartedComponent$.next(getStartedComponent);
},
getBreadcrumbsNav$: () => breadcrumbsNav$,
getUpselling: () => this.upsellingService,
};
}

View file

@ -169,7 +169,6 @@ export type StartServices = CoreStart &
export interface PluginSetup {
resolver: () => Promise<ResolverPluginSetup>;
upselling: UpsellingService;
setAppLinksSwitcher: (appLinksSwitcher: AppLinksSwitcher) => void;
}
@ -179,6 +178,7 @@ export interface PluginStart {
setIsSidebarEnabled: (isSidebarEnabled: boolean) => void;
setGetStartedPage: (getStartedComponent: React.ComponentType) => void;
getBreadcrumbsNav$: () => Observable<BreadcrumbsNav>;
getUpselling: () => UpsellingService;
}
export interface AppObservableLibs {

View file

@ -9,7 +9,8 @@
"browser": true,
"configPath": ["xpack", "securitySolutionEss"],
"requiredPlugins": [
"securitySolution"
"securitySolution",
"licensing",
],
"optionalPlugins": [
"cloudExperiments",

View file

@ -11,6 +11,7 @@ import {
KibanaContextProvider,
useKibana as useKibanaReact,
} from '@kbn/kibana-react-plugin/public';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { SecuritySolutionEssPluginStartDeps } from '../types';
export type Services = CoreStart & SecuritySolutionEssPluginStartDeps;
@ -18,7 +19,11 @@ export type Services = CoreStart & SecuritySolutionEssPluginStartDeps;
export const KibanaServicesProvider: React.FC<{
services: Services;
}> = ({ services, children }) => {
return <KibanaContextProvider services={services}>{children}</KibanaContextProvider>;
return (
<KibanaContextProvider services={services}>
<NavigationProvider core={services}>{children}</NavigationProvider>
</KibanaContextProvider>
);
};
export const useKibana = () => useKibanaReact<Services>();
@ -29,3 +34,16 @@ export const createServices = (
): Services => {
return { ...core, ...pluginsStart };
};
export const withServicesProvider = <T extends object>(
Component: React.ComponentType<T>,
services: Services
) => {
return function WithServicesProvider(props: T) {
return (
<KibanaServicesProvider services={services}>
<Component {...props} />
</KibanaServicesProvider>
);
};
};

View file

@ -4,16 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type React from 'react';
import { KibanaServicesProvider, type Services } from '../common/services';
import { withServicesProvider, type Services } from '../common/services';
import { GetStarted } from './lazy';
export const getSecurityGetStartedComponent = (services: Services): React.ComponentType =>
function GetStartedComponent() {
return (
<KibanaServicesProvider services={services}>
<GetStarted />
</KibanaServicesProvider>
);
};
withServicesProvider(GetStarted, services);

View file

@ -9,6 +9,7 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { subscribeBreadcrumbs } from './breadcrumbs';
import { createServices } from './common/services';
import { getSecurityGetStartedComponent } from './get_started';
import { registerUpsellings } from './upselling/register_upsellings';
import type {
SecuritySolutionEssPluginSetup,
SecuritySolutionEssPluginStart,
@ -36,9 +37,13 @@ export class SecuritySolutionEssPlugin
core: CoreStart,
startDeps: SecuritySolutionEssPluginStartDeps
): SecuritySolutionEssPluginStart {
const { securitySolution } = startDeps;
const { securitySolution, licensing } = startDeps;
const services = createServices(core, startDeps);
licensing.license$.subscribe((license) => {
registerUpsellings(securitySolution.getUpselling(), license, services);
});
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services));
subscribeBreadcrumbs(services);

View file

@ -10,6 +10,7 @@ import type {
PluginStart as SecuritySolutionPluginStart,
} from '@kbn/security-solution-plugin/public';
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SecuritySolutionEssPluginSetup {}
@ -24,4 +25,5 @@ export interface SecuritySolutionEssPluginSetupDeps {
export interface SecuritySolutionEssPluginStartDeps {
securitySolution: SecuritySolutionPluginStart;
cloudExperiments?: CloudExperimentsPluginStart;
licensing: LicensingPluginStart;
}

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const UPGRADE_INVESTIGATION_GUIDE = (requiredLicense: string) =>
i18n.translate('xpack.securitySolutionEss.markdown.insight.upsell', {
defaultMessage: 'Upgrade to {requiredLicense} to make use of insights in investigation guides',
values: {
requiredLicense,
},
});

View file

@ -0,0 +1,107 @@
/*
* 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, { useCallback } from 'react';
import {
EuiCard,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiTextColor,
EuiImage,
EuiPageHeader,
EuiSpacer,
} from '@elastic/eui';
import styled from '@emotion/styled';
import { useNavigation } from '@kbn/security-solution-navigation';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import * as i18n from './translations';
import paywallPng from '../../common/images/entity_paywall.png';
const PaywallDiv = styled.div`
max-width: 75%;
margin: 0 auto;
.euiCard__betaBadgeWrapper {
.euiCard__betaBadge {
width: auto;
}
}
.platinumCardDescription {
padding: 0 15%;
}
`;
const StyledEuiCard = styled(EuiCard)`
span.euiTitle {
max-width: 540px;
display: block;
margin: 0 auto;
}
`;
const EntityAnalyticsUpsellingComponent = () => {
const { getAppUrl, navigateTo } = useNavigation();
const subscriptionUrl = getAppUrl({
appId: 'management',
path: 'stack/license_management',
});
const goToSubscription = useCallback(() => {
navigateTo({ url: subscriptionUrl });
}, [navigateTo, subscriptionUrl]);
return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
<EuiPageHeader pageTitle={i18n.ENTITY_ANALYTICS_TITLE} />
<EuiSpacer size="xl" />
<PaywallDiv>
<StyledEuiCard
data-test-subj="platinumCard"
betaBadgeProps={{ label: i18n.PLATINUM }}
icon={<EuiIcon size="xl" type="lock" />}
display="subdued"
title={
<h3>
<strong>{i18n.ENTITY_ANALYTICS_LICENSE_DESC}</strong>
</h3>
}
description={false}
paddingSize="xl"
>
<EuiFlexGroup className="platinumCardDescription" direction="column" gutterSize="none">
<EuiText>
<EuiFlexItem>
<p>
<EuiTextColor color="subdued">{i18n.UPGRADE_MESSAGE}</EuiTextColor>
</p>
</EuiFlexItem>
<EuiFlexItem>
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON}
</EuiButton>
</div>
</EuiFlexItem>
</EuiText>
</EuiFlexGroup>
</StyledEuiCard>
<EuiFlexGroup>
<EuiFlexItem>
<EuiImage alt={i18n.UPGRADE_MESSAGE} src={paywallPng} size="fullWidth" />
</EuiFlexItem>
</EuiFlexGroup>
</PaywallDiv>
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);
};
EntityAnalyticsUpsellingComponent.displayName = 'EntityAnalyticsUpsellingComponent';
// eslint-disable-next-line import/no-default-export
export default React.memo(EntityAnalyticsUpsellingComponent);

View file

@ -7,14 +7,28 @@
import { i18n } from '@kbn/i18n';
export const PLATINUM = i18n.translate('xpack.securitySolution.paywall.platinum', {
export const PLATINUM = i18n.translate('xpack.securitySolutionEss.paywall.platinum', {
defaultMessage: 'Platinum',
});
export const UPGRADE_MESSAGE = i18n.translate('xpack.securitySolution.paywall.upgradeMessage', {
export const UPGRADE_MESSAGE = i18n.translate('xpack.securitySolutionEss.paywall.upgradeMessage', {
defaultMessage: 'This feature is available with Platinum or higher subscription',
});
export const UPGRADE_BUTTON = i18n.translate('xpack.securitySolution.paywall.upgradeButton', {
export const UPGRADE_BUTTON = i18n.translate('xpack.securitySolutionEss.paywall.upgradeButton', {
defaultMessage: 'Upgrade to Platinum',
});
export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate(
'xpack.securitySolutionEss.entityAnalytics.pageDesc',
{
defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics',
}
);
export const ENTITY_ANALYTICS_TITLE = i18n.translate(
'xpack.securitySolutionEss.navigation.entityAnalytics',
{
defaultMessage: 'Entity Analytics',
}
);

View file

@ -0,0 +1,102 @@
/*
* 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 { SecurityPageName } from '@kbn/security-solution-plugin/common';
import type { UpsellingService } from '@kbn/security-solution-plugin/public';
import type {
MessageUpsellings,
PageUpsellings,
SectionUpsellings,
UpsellingMessageId,
UpsellingSectionId,
} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types';
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public';
import { lazy } from 'react';
import type React from 'react';
import { UPGRADE_INVESTIGATION_GUIDE } from './messages/investigation_guide_upselling';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
const EntityAnalyticsUpsellingLazy = lazy(() => import('./pages/entity_analytics_upselling'));
interface UpsellingsConfig {
minimumLicenseRequired: LicenseType;
component: React.ComponentType;
}
interface UpsellingsMessageConfig {
minimumLicenseRequired: LicenseType;
message: string;
id: UpsellingMessageId;
}
type UpsellingPages = Array<UpsellingsConfig & { pageName: SecurityPageName }>;
type UpsellingSections = Array<UpsellingsConfig & { id: UpsellingSectionId }>;
type UpsellingMessages = UpsellingsMessageConfig[];
export const registerUpsellings = (
upselling: UpsellingService,
license: ILicense,
services: Services
) => {
const upsellingPagesToRegister = upsellingPages.reduce<PageUpsellings>(
(pageUpsellings, { pageName, minimumLicenseRequired, component }) => {
if (!license.hasAtLeast(minimumLicenseRequired)) {
pageUpsellings[pageName] = withServicesProvider(component, services);
}
return pageUpsellings;
},
{}
);
const upsellingSectionsToRegister = upsellingSections.reduce<SectionUpsellings>(
(sectionUpsellings, { id, minimumLicenseRequired, component }) => {
if (!license.hasAtLeast(minimumLicenseRequired)) {
sectionUpsellings[id] = component;
}
return sectionUpsellings;
},
{}
);
const upsellingMessagesToRegister = upsellingMessages.reduce<MessageUpsellings>(
(messagesUpsellings, { id, minimumLicenseRequired, message }) => {
if (!license.hasAtLeast(minimumLicenseRequired)) {
messagesUpsellings[id] = message;
}
return messagesUpsellings;
},
{}
);
upselling.setPages(upsellingPagesToRegister);
upselling.setSections(upsellingSectionsToRegister);
upselling.setMessages(upsellingMessagesToRegister);
};
// Upsellings for entire pages, linked to a SecurityPageName
export const upsellingPages: UpsellingPages = [
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
{
pageName: SecurityPageName.entityAnalytics,
minimumLicenseRequired: 'platinum',
component: EntityAnalyticsUpsellingLazy,
},
];
// Upsellings for sections, linked by arbitrary ids
export const upsellingSections: UpsellingSections = [
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
];
// Upsellings for sections, linked by arbitrary ids
export const upsellingMessages: UpsellingMessages = [
{
id: 'investigation_guide',
minimumLicenseRequired: 'platinum',
message: UPGRADE_INVESTIGATION_GUIDE('platinum'),
},
];

View file

@ -19,5 +19,8 @@
"@kbn/i18n",
"@kbn/cloud-experiments-plugin",
"@kbn/kibana-react-plugin",
"@kbn/security-solution-navigation",
"@kbn/licensing-plugin",
"@kbn/shared-ux-page-kibana-template",
]
}

View file

@ -9,8 +9,6 @@ import { EuiTitle, useEuiTheme, useEuiShadow } from '@elastic/eui';
import React from 'react';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { css } from '@emotion/react';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { WelcomePanel } from './welcome_panel';
import { TogglePanel } from './toggle_panel';
import {
@ -114,17 +112,15 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes })
padding: 0 ${euiTheme.base * 2.25}px;
`}
>
<NavigationProvider core={services}>
<TogglePanel
finishedSteps={finishedSteps}
activeSections={activeSections}
activeProducts={activeProducts}
expandedCardSteps={expandedCardSteps}
onStepClicked={onStepClicked}
onCardClicked={onCardClicked}
onStepButtonClicked={onStepButtonClicked}
/>
</NavigationProvider>
<TogglePanel
finishedSteps={finishedSteps}
activeSections={activeSections}
activeProducts={activeProducts}
expandedCardSteps={expandedCardSteps}
onStepClicked={onStepClicked}
onCardClicked={onCardClicked}
onStepButtonClicked={onStepButtonClicked}
/>
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);

View file

@ -40,7 +40,6 @@ export class SecuritySolutionServerlessPlugin
_core: CoreSetup,
setupDeps: SecuritySolutionServerlessPluginSetupDeps
): SecuritySolutionServerlessPluginSetup {
registerUpsellings(setupDeps.securitySolution.upselling, this.config.productTypes);
setupDeps.securitySolution.setAppLinksSwitcher(projectAppLinksSwitcher);
return {};
@ -55,6 +54,7 @@ export class SecuritySolutionServerlessPlugin
const services = createServices(core, startDeps);
registerUpsellings(securitySolution.getUpselling(), this.config.productTypes);
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes));
configureNavigation(services, this.config);

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 React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
const withSuspenseUpsell = <T extends object = {}>(
Component: React.ComponentType<T>
): React.FC<T> =>
function WithSuspenseUpsell(props) {
return (
<Suspense fallback={<EuiLoadingSpinner size="s" />}>
<Component {...props} />
</Suspense>
);
};
export const ThreatIntelligencePaywallLazy = withSuspenseUpsell(
lazy(() => import('./pages/threat_intelligence_paywall'))
);
export const OsqueryResponseActionsUpsellingSectionLazy = withSuspenseUpsell(
lazy(() => import('./pages/osquery_automated_response_actions'))
);

View file

@ -22,6 +22,3 @@ export const investigationGuideUpselling = (requiredPLI: AppFeatureKey): string
const productTypeRequired = getProductTypeByPLI(requiredPLI);
return productTypeRequired ? UPGRADE_INVESTIGATION_GUIDE(productTypeRequired) : '';
};
// eslint-disable-next-line import/no-default-export
export { investigationGuideUpselling as default };

View file

@ -31,58 +31,58 @@ describe('registerUpsellings', () => {
it('should not register anything when all PLIs features are enabled', () => {
mockGetProductAppFeatures.mockReturnValue(ALL_APP_FEATURE_KEYS);
const registerPages = jest.fn();
const registerSections = jest.fn();
const registerMessages = jest.fn();
const setPages = jest.fn();
const setSections = jest.fn();
const setMessages = jest.fn();
const upselling = {
registerPages,
registerSections,
registerMessages,
setPages,
setSections,
setMessages,
} as unknown as UpsellingService;
registerUpsellings(upselling, allProductTypes);
expect(registerPages).toHaveBeenCalledTimes(1);
expect(registerPages).toHaveBeenCalledWith({});
expect(setPages).toHaveBeenCalledTimes(1);
expect(setPages).toHaveBeenCalledWith({});
expect(registerSections).toHaveBeenCalledTimes(1);
expect(registerSections).toHaveBeenCalledWith({});
expect(setSections).toHaveBeenCalledTimes(1);
expect(setSections).toHaveBeenCalledWith({});
expect(registerMessages).toHaveBeenCalledTimes(1);
expect(registerMessages).toHaveBeenCalledWith({});
expect(setMessages).toHaveBeenCalledTimes(1);
expect(setMessages).toHaveBeenCalledWith({});
});
it('should register all upsellings pages, sections and messages when PLIs features are disabled', () => {
mockGetProductAppFeatures.mockReturnValue([]);
const registerPages = jest.fn();
const registerSections = jest.fn();
const registerMessages = jest.fn();
const setPages = jest.fn();
const setSections = jest.fn();
const setMessages = jest.fn();
const upselling = {
registerPages,
registerSections,
registerMessages,
setPages,
setSections,
setMessages,
} as unknown as UpsellingService;
registerUpsellings(upselling, allProductTypes);
const expectedPagesObject = Object.fromEntries(
upsellingPages.map(({ pageName }) => [pageName, expect.any(Object)])
upsellingPages.map(({ pageName }) => [pageName, expect.anything()])
);
expect(registerPages).toHaveBeenCalledTimes(1);
expect(registerPages).toHaveBeenCalledWith(expectedPagesObject);
expect(setPages).toHaveBeenCalledTimes(1);
expect(setPages).toHaveBeenCalledWith(expectedPagesObject);
const expectedSectionsObject = Object.fromEntries(
upsellingSections.map(({ id }) => [id, expect.any(Object)])
upsellingSections.map(({ id }) => [id, expect.anything()])
);
expect(registerSections).toHaveBeenCalledTimes(1);
expect(registerSections).toHaveBeenCalledWith(expectedSectionsObject);
expect(setSections).toHaveBeenCalledTimes(1);
expect(setSections).toHaveBeenCalledWith(expectedSectionsObject);
const expectedMessagesObject = Object.fromEntries(
upsellingMessages.map(({ id }) => [id, expect.any(String)])
);
expect(registerMessages).toHaveBeenCalledTimes(1);
expect(registerMessages).toHaveBeenCalledWith(expectedMessagesObject);
expect(setMessages).toHaveBeenCalledTimes(1);
expect(setMessages).toHaveBeenCalledWith(expectedMessagesObject);
});
});

View file

@ -15,37 +15,19 @@ import type {
MessageUpsellings,
UpsellingMessageId,
} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types';
import React, { lazy } from 'react';
import React from 'react';
import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
import { getProductAppFeatures } from '../../common/pli/pli_features';
import investigationGuideUpselling from './pages/investigation_guide_upselling';
const ThreatIntelligencePaywallLazy = lazy(async () => {
const ThreatIntelligencePaywall = (await import('./pages/threat_intelligence_paywall')).default;
return {
default: () => <ThreatIntelligencePaywall requiredPLI={AppFeatureKey.threatIntelligence} />,
};
});
const OsqueryResponseActionsUpsellingSectionlLazy = lazy(async () => {
const OsqueryResponseActionsUpsellingSection = (
await import('./pages/osquery_automated_response_actions')
).default;
return {
default: () => (
<OsqueryResponseActionsUpsellingSection
requiredPLI={AppFeatureKey.osqueryAutomatedResponseActions}
/>
),
};
});
import { investigationGuideUpselling } from './messages/investigation_guide_upselling';
import {
OsqueryResponseActionsUpsellingSectionLazy,
ThreatIntelligencePaywallLazy,
} from './lazy_upselling';
interface UpsellingsConfig {
pli: AppFeatureKey;
component: React.LazyExoticComponent<React.ComponentType>;
component: React.ComponentType;
}
interface UpsellingsMessageConfig {
@ -94,42 +76,35 @@ export const registerUpsellings = (
{}
);
upselling.registerPages(upsellingPagesToRegister);
upselling.registerSections(upsellingSectionsToRegister);
upselling.registerMessages(upsellingMessagesToRegister);
upselling.setPages(upsellingPagesToRegister);
upselling.setSections(upsellingSectionsToRegister);
upselling.setMessages(upsellingMessagesToRegister);
};
// Upsellings for entire pages, linked to a SecurityPageName
export const upsellingPages: UpsellingPages = [
// Sample code for registering a Upselling page
// Make sure the component is lazy loaded `const GenericUpsellingPageLazy = lazy(() => import('./pages/generic_upselling_page'));`
// {
// pageName: SecurityPageName.entityAnalytics,
// pli: AppFeatureKey.advancedInsights,
// component: () => <GenericUpsellingPageLazy requiredPLI={AppFeatureKey.advancedInsights} />,
// },
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
{
pageName: SecurityPageName.threatIntelligence,
pli: AppFeatureKey.threatIntelligence,
component: ThreatIntelligencePaywallLazy,
component: () => (
<ThreatIntelligencePaywallLazy requiredPLI={AppFeatureKey.threatIntelligence} />
),
},
];
// Upsellings for sections, linked by arbitrary ids
export const upsellingSections: UpsellingSections = [
// Sample code for registering a Upselling section
// Make sure the component is lazy loaded `const GenericUpsellingSectionLazy = lazy(() => import('./pages/generic_upselling_section'));`
// {
// id: 'entity_analytics_panel',
// pli: AppFeatureKey.advancedInsights,
// component: () => <GenericUpsellingSectionLazy requiredPLI={AppFeatureKey.advancedInsights} />,
// },
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
{
id: 'osquery_automated_response_actions',
pli: AppFeatureKey.osqueryAutomatedResponseActions,
component: OsqueryResponseActionsUpsellingSectionlLazy,
component: () => (
<OsqueryResponseActionsUpsellingSectionLazy
requiredPLI={AppFeatureKey.osqueryAutomatedResponseActions}
/>
),
},
{
id: 'endpointPolicyProtections',
pli: AppFeatureKey.endpointPolicyProtections,

View file

@ -32976,7 +32976,6 @@
"xpack.securitySolution.entityAnalytics.header.criticalUsers": "Utilisateurs critiques",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "Le tableau des risques de l'hôte n'est pas affecté par la plage temporelle. Ce tableau montre le dernier score de risque enregistré pour chaque hôte.",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "Scores de risque de l'hôte",
"xpack.securitySolution.entityAnalytics.pageDesc": "Détecter les menaces des utilisateurs et des hôtes de votre réseau avec l'Analyse des entités",
"xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "Le panneau de Score de risque de l'hôte affiche la liste des hôtes à risque ainsi que leur dernier score de risque. Vous pouvez filtrer cette liste à laide de filtres globaux dans la barre de recherche KQL. Le filtre de sélecteur de plage temporelle affiche les alertes dans lintervalle de temps sélectionné uniquement et ne filtre pas la liste des hôtes à risque.",
"xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "En savoir plus",
"xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "En version d'évaluation technique",
@ -33664,7 +33663,6 @@
"xpack.securitySolution.markdown.insight.relativeTimerange": "Plage temporelle relative",
"xpack.securitySolution.markdown.insight.relativeTimerangeText": "Sélectionnez une plage horaire pour limiter la requête, par rapport à l'heure de création de l'alerte (facultatif).",
"xpack.securitySolution.markdown.insight.title": "Examiner",
"xpack.securitySolution.markdown.insight.upsell": "Mettez à niveau vers Platinum pour pouvoir utiliser les informations exploitables dans des guides dinvestigation",
"xpack.securitySolution.markdown.invalid": "Markdown non valide détecté",
"xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "Ajouter une recherche",
"xpack.securitySolution.markdown.osquery.addModalTitle": "Ajouter une recherche",
@ -33948,9 +33946,6 @@
"xpack.securitySolution.paginatedTable.showingSubtitle": "Affichant",
"xpack.securitySolution.paginatedTable.tooManyResultsToastText": "Affiner votre recherche pour mieux filtrer les résultats",
"xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - trop de résultats",
"xpack.securitySolution.paywall.platinum": "Platinum",
"xpack.securitySolution.paywall.upgradeButton": "Mettre à niveau vers Platinum",
"xpack.securitySolution.paywall.upgradeMessage": "Cette fonctionnalité est disponible avec l'abonnement Platinum ou supérieur",
"xpack.securitySolution.policiesTab": "Politiques",
"xpack.securitySolution.policy.backToPolicyList": "Retour à la liste des politiques",
"xpack.securitySolution.policy.list.createdAt": "Date de création",

View file

@ -32975,7 +32975,6 @@
"xpack.securitySolution.entityAnalytics.header.criticalUsers": "重要なユーザー",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "ホストリスク表は時間範囲の影響を受けません。この表は、各ホストの最後に記録されたリスクスコアを示します。",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "ホストリスクスコア",
"xpack.securitySolution.entityAnalytics.pageDesc": "Entity Analyticsを使用して、ネットワーク内のユーザーとホストから脅威を検出",
"xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "ホストリスクスコアパネルには、リスクのあるホストの一覧と最新のリスクスコアが表示されます。KQL検索バーのグローバルフィルターを使って、この一覧をフィルタリングできます。時間範囲ピッカーフィルターは、選択した時間範囲内のアラートのみを表示し、リスクのあるホストの一覧をフィルタリングしません。",
"xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "詳細",
"xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "テクニカルプレビュー",
@ -33663,7 +33662,6 @@
"xpack.securitySolution.markdown.insight.relativeTimerange": "相対的時間範囲",
"xpack.securitySolution.markdown.insight.relativeTimerangeText": "アラートの作成日時に相対的な、クエリを限定するための時間範囲を選択します(任意)。",
"xpack.securitySolution.markdown.insight.title": "調査",
"xpack.securitySolution.markdown.insight.upsell": "プラチナにアップグレードして、調査ガイドのインサイトを利用",
"xpack.securitySolution.markdown.invalid": "無効なマークダウンが検出されました",
"xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "クエリを追加",
"xpack.securitySolution.markdown.osquery.addModalTitle": "クエリを追加",
@ -33947,9 +33945,6 @@
"xpack.securitySolution.paginatedTable.showingSubtitle": "表示中",
"xpack.securitySolution.paginatedTable.tooManyResultsToastText": "クエリ範囲を縮めて結果をさらにフィルタリングしてください",
"xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 結果が多すぎます",
"xpack.securitySolution.paywall.platinum": "プラチナ",
"xpack.securitySolution.paywall.upgradeButton": "プラチナにアップグレード",
"xpack.securitySolution.paywall.upgradeMessage": "この機能は、プラチナ以上のサブスクリプションでご利用いただけます",
"xpack.securitySolution.policiesTab": "ポリシー",
"xpack.securitySolution.policy.backToPolicyList": "ポリシーリストに戻る",
"xpack.securitySolution.policy.list.createdAt": "作成日",

View file

@ -32971,7 +32971,6 @@
"xpack.securitySolution.entityAnalytics.header.criticalUsers": "关键用户",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "主机风险表不受时间范围影响。本表显示每台主机最新记录的风险分数。",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "主机风险分数",
"xpack.securitySolution.entityAnalytics.pageDesc": "通过实体分析检测来自您网络中用户和主机的威胁",
"xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "“主机风险分数”面板显示有风险主机及其最新风险分数的列表。可以在 KQL 搜索栏中使用全局筛选来筛选此列表。时间范围选取器筛选将仅显示选定时间范围内的告警,并且不筛选有风险主机列表。",
"xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "了解详情",
"xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "处于技术预览状态",
@ -33659,7 +33658,6 @@
"xpack.securitySolution.markdown.insight.relativeTimerange": "相对时间范围",
"xpack.securitySolution.markdown.insight.relativeTimerangeText": "选择相对于告警创建时间的时间范围(可选)以限制查询。",
"xpack.securitySolution.markdown.insight.title": "调查",
"xpack.securitySolution.markdown.insight.upsell": "升级到白金级以利用调查指南中的洞见",
"xpack.securitySolution.markdown.invalid": "检测到无效 Markdown",
"xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "添加查询",
"xpack.securitySolution.markdown.osquery.addModalTitle": "添加查询",
@ -33943,9 +33941,6 @@
"xpack.securitySolution.paginatedTable.showingSubtitle": "正在显示",
"xpack.securitySolution.paginatedTable.tooManyResultsToastText": "缩减您的查询范围,以更好地筛选结果",
"xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 结果过多",
"xpack.securitySolution.paywall.platinum": "白金级",
"xpack.securitySolution.paywall.upgradeButton": "升级到白金级",
"xpack.securitySolution.paywall.upgradeMessage": "白金级或更高级订阅可以使用此功能",
"xpack.securitySolution.policiesTab": "策略",
"xpack.securitySolution.policy.backToPolicyList": "返回到策略列表",
"xpack.securitySolution.policy.list.createdAt": "创建日期",