Create upselling package and implement EntityAnalytics serverless upselling (#164136)

UX: https://github.com/elastic/security-team/issues/7310

## Summary

* It creates an Upselling package to share the service and components
between ESS and Serverless plugins
* It implements upselling for entity analytics on serverless by
replicating the ESS approach

ESS
<img width="1520" alt="Screenshot 2023-08-17 at 13 34 59"
src="95c2c94e-7ab3-4d9f-aa24-b3e9c00eb3ed">

Serverless
<img width="1523" alt="Screenshot 2023-08-17 at 13 39 25"
src="618ce9dc-ef4e-469d-884a-dfb09834d0b0">

We are not displaying the upgrade button because we still don't know how
to link to the cloud settings page.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2023-08-24 13:18:19 +02:00 committed by GitHub
parent 1610e32972
commit 75644797c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 319 additions and 236 deletions

1
.github/CODEOWNERS vendored
View file

@ -604,6 +604,7 @@ x-pack/plugins/security_solution @elastic/security-solution
x-pack/plugins/security_solution_serverless @elastic/security-solution
x-pack/packages/security-solution/side_nav @elastic/security-threat-hunting-explore
x-pack/packages/security-solution/storybook/config @elastic/security-threat-hunting-explore
x-pack/packages/security-solution/upselling @elastic/security-threat-hunting-explore
x-pack/test/security_functional/plugins/test_endpoints @elastic/kibana-security
packages/kbn-securitysolution-autocomplete @elastic/security-detection-engine
x-pack/packages/security-solution/data_table @elastic/security-threat-hunting-investigations

View file

@ -608,6 +608,7 @@
"@kbn/security-solution-serverless": "link:x-pack/plugins/security_solution_serverless",
"@kbn/security-solution-side-nav": "link:x-pack/packages/security-solution/side_nav",
"@kbn/security-solution-storybook-config": "link:x-pack/packages/security-solution/storybook/config",
"@kbn/security-solution-upselling": "link:x-pack/packages/security-solution/upselling",
"@kbn/security-test-endpoints-plugin": "link:x-pack/test/security_functional/plugins/test_endpoints",
"@kbn/securitysolution-autocomplete": "link:packages/kbn-securitysolution-autocomplete",
"@kbn/securitysolution-data-table": "link:x-pack/packages/security-solution/data_table",

View file

@ -1202,6 +1202,8 @@
"@kbn/security-solution-side-nav/*": ["x-pack/packages/security-solution/side_nav/*"],
"@kbn/security-solution-storybook-config": ["x-pack/packages/security-solution/storybook/config"],
"@kbn/security-solution-storybook-config/*": ["x-pack/packages/security-solution/storybook/config/*"],
"@kbn/security-solution-upselling": ["x-pack/packages/security-solution/upselling"],
"@kbn/security-solution-upselling/*": ["x-pack/packages/security-solution/upselling/*"],
"@kbn/security-test-endpoints-plugin": ["x-pack/test/security_functional/plugins/test_endpoints"],
"@kbn/security-test-endpoints-plugin/*": ["x-pack/test/security_functional/plugins/test_endpoints/*"],
"@kbn/securitysolution-autocomplete": ["packages/kbn-securitysolution-autocomplete"],

View file

@ -0,0 +1,3 @@
## Security Solution Upselling
This package contains the upselling service that registers pages/component/messages and shared upselling components for ESS and Serverless plugins.

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/security-solution/upselling'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/security-solution-upselling",
"owner": "@elastic/security-threat-hunting-explore"
}

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
export const UPGRADE_INVESTIGATION_GUIDE = (requiredLicense: string) =>
i18n.translate('xpack.securitySolutionEss.markdown.insight.upsell', {
i18n.translate('securitySolutionPackages.markdown.insight.upsell', {
defaultMessage: 'Upgrade to {requiredLicense} to make use of insights in investigation guides',
values: {
requiredLicense,

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/security-solution-upselling",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"
}

View file

@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render } from '@testing-library/react';
import EntityAnalyticsUpsellingComponent from './entity_analytics';
jest.mock('@kbn/security-solution-navigation', () => {
const original = jest.requireActual('@kbn/security-solution-navigation');
return {
...original,
useNavigation: () => ({
navigateTo: jest.fn(),
}),
};
});
describe('EntityAnalyticsUpselling', () => {
it('should render', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
);
expect(getByTestId('paywallCardDescription')).toBeInTheDocument();
});
it('should throw exception when requiredLicense and requiredProduct are not provided', () => {
expect(() => render(<EntityAnalyticsUpsellingComponent />)).toThrow();
});
it('should show product message when requiredProduct is provided', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent
requiredProduct="TEST PRODUCT"
requiredLicense="TEST LICENSE"
/>
);
expect(getByTestId('paywallCardDescription')).toHaveTextContent(
'Entity risk scoring capability is available in our TEST PRODUCT license tier'
);
});
it('should show product badge when requiredProduct is provided', () => {
const { getByText } = render(
<EntityAnalyticsUpsellingComponent
requiredProduct="TEST PRODUCT"
requiredLicense="TEST LICENSE"
/>
);
expect(getByText('TEST PRODUCT')).toBeInTheDocument();
});
it('should show license message when requiredLicense is provided', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
);
expect(getByTestId('paywallCardDescription')).toHaveTextContent(
'This feature is available with TEST LICENSE or higher subscription'
);
});
it('should show license badge when requiredLicense is provided', () => {
const { getByText } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
);
expect(getByText('TEST LICENSE')).toBeInTheDocument();
});
});

View file

@ -23,7 +23,7 @@ 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';
import paywallPng from '../images/entity_paywall.png';
const PaywallDiv = styled.div`
max-width: 75%;
@ -33,7 +33,7 @@ const PaywallDiv = styled.div`
width: auto;
}
}
.platinumCardDescription {
.paywallCardDescription {
padding: 0 15%;
}
`;
@ -45,15 +45,31 @@ const StyledEuiCard = styled(EuiCard)`
}
`;
const EntityAnalyticsUpsellingComponent = () => {
const { getAppUrl, navigateTo } = useNavigation();
const subscriptionUrl = getAppUrl({
appId: 'management',
path: 'stack/license_management',
});
const EntityAnalyticsUpsellingComponent = ({
requiredLicense,
requiredProduct,
subscriptionUrl,
}: {
requiredLicense?: string;
requiredProduct?: string;
subscriptionUrl?: string;
}) => {
const { navigateTo } = useNavigation();
const goToSubscription = useCallback(() => {
navigateTo({ url: subscriptionUrl });
}, [navigateTo, subscriptionUrl]);
if (!requiredProduct && !requiredLicense) {
throw new Error('requiredProduct or requiredLicense must be defined');
}
const upgradeMessage = requiredProduct
? i18n.UPGRADE_PRODUCT_MESSAGE(requiredProduct)
: i18n.UPGRADE_LICENSE_MESSAGE(requiredLicense ?? '');
const requiredProductOrLicense = requiredProduct ?? requiredLicense ?? '';
return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
@ -61,8 +77,7 @@ const EntityAnalyticsUpsellingComponent = () => {
<EuiSpacer size="xl" />
<PaywallDiv>
<StyledEuiCard
data-test-subj="platinumCard"
betaBadgeProps={{ label: i18n.PLATINUM }}
betaBadgeProps={{ label: requiredProductOrLicense }}
icon={<EuiIcon size="xl" type="lock" />}
display="subdued"
title={
@ -73,26 +88,33 @@ const EntityAnalyticsUpsellingComponent = () => {
description={false}
paddingSize="xl"
>
<EuiFlexGroup className="platinumCardDescription" direction="column" gutterSize="none">
<EuiFlexGroup
data-test-subj="paywallCardDescription"
className="paywallCardDescription"
direction="column"
gutterSize="none"
>
<EuiText>
<EuiFlexItem>
<p>
<EuiTextColor color="subdued">{i18n.UPGRADE_MESSAGE}</EuiTextColor>
<EuiTextColor color="subdued">{upgradeMessage}</EuiTextColor>
</p>
</EuiFlexItem>
<EuiFlexItem>
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON}
</EuiButton>
</div>
{subscriptionUrl && (
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON(requiredProductOrLicense)}
</EuiButton>
</div>
)}
</EuiFlexItem>
</EuiText>
</EuiFlexGroup>
</StyledEuiCard>
<EuiFlexGroup>
<EuiFlexItem>
<EuiImage alt={i18n.UPGRADE_MESSAGE} src={paywallPng} size="fullWidth" />
<EuiImage alt={upgradeMessage} src={paywallPng} size="fullWidth" />
</EuiFlexItem>
</EuiFlexGroup>
</PaywallDiv>

View file

@ -0,0 +1,47 @@
/*
* 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_LICENSE_MESSAGE = (requiredLicense: string) =>
i18n.translate('securitySolutionPackages.entityAnalytics.paywall.upgradeLicenseMessage', {
defaultMessage: 'This feature is available with {requiredLicense} or higher subscription',
values: {
requiredLicense,
},
});
export const UPGRADE_PRODUCT_MESSAGE = (requiredProduct: string) =>
i18n.translate('securitySolutionPackages.entityAnalytics.paywall.upgradeProductMessage', {
defaultMessage:
'Entity risk scoring capability is available in our {requiredProduct} license tier',
values: {
requiredProduct,
},
});
export const UPGRADE_BUTTON = (requiredLicenseOrProduct: string) =>
i18n.translate('securitySolutionPackages.entityAnalytics.paywall.upgradeButton', {
defaultMessage: 'Upgrade to {requiredLicenseOrProduct}',
values: {
requiredLicenseOrProduct,
},
});
export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate(
'securitySolutionPackages.entityAnalytics.pageDesc',
{
defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics',
}
);
export const ENTITY_ANALYTICS_TITLE = i18n.translate(
'securitySolutionPackages.entityAnalytics.navigation',
{
defaultMessage: 'Entity Analytics',
}
);

View file

@ -5,4 +5,9 @@
* 2.0.
*/
export { UpsellingService } from './upselling_service';
export type { PageUpsellings, SectionUpsellings, UpsellingSectionId } from './types';
export type {
PageUpsellings,
SectionUpsellings,
UpsellingSectionId,
UpsellingMessageId,
} from './types';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { SecurityPageName } from '../../../../common';
import type { SecurityPageName } from '@kbn/security-solution-navigation';
export type PageUpsellings = Partial<Record<SecurityPageName, React.ComponentType>>;
export type MessageUpsellings = Partial<Record<UpsellingMessageId, string>>;

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { firstValueFrom } from 'rxjs';
import { SecurityPageName } from '../../../../common';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { UpsellingService } from './upselling_service';
const TestComponent = () => <div>{'TEST component'}</div>;

View file

@ -7,7 +7,7 @@
import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type { SecurityPageName } from '../../../../common';
import type { SecurityPageName } from '@kbn/security-solution-navigation';
import type {
SectionUpsellings,
PageUpsellings,

View file

@ -0,0 +1,27 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@emotion/react/types/css-prop",
"@testing-library/jest-dom",
"@testing-library/react",
"@kbn/ambient-ui-types"
]
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"kbn_references": [
"@kbn/i18n",
"@kbn/security-solution-navigation",
"@kbn/shared-ux-page-kibana-template",
],
"exclude": [
"target/**/*"
]
}

View file

@ -11,7 +11,7 @@ import type { AppLinkItems } from '../../links';
import { updateAppLinks } from '../../links';
import { mockGlobalState } from '../../mock';
import type { Capabilities } from '@kbn/core-capabilities-common';
import { UpsellingService } from '../../lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
const defaultAppLinks: AppLinkItems = [
{

View file

@ -6,7 +6,7 @@
*/
import React, { memo, useContext } from 'react';
import type { UpsellingService } from '../../..';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
export const UpsellingProviderContext = React.createContext<UpsellingService | null>(null);

View file

@ -8,7 +8,7 @@
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { SecurityPageName } from '../../../common';
import { UpsellingService } from '../lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { useUpsellingComponent, useUpsellingMessage, useUpsellingPage } from './use_upselling';
import { UpsellingProvider } from '../components/upselling_provider';

View file

@ -8,10 +8,12 @@
import { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type React from 'react';
import type {
UpsellingSectionId,
UpsellingMessageId,
} from '@kbn/security-solution-upselling/service';
import { useUpsellingService } from '../components/upselling_provider';
import type { UpsellingSectionId } from '../lib/upsellings';
import type { SecurityPageName } from '../../../common';
import type { UpsellingMessageId } from '../lib/upsellings/types';
export const useUpsellingComponent = (id: UpsellingSectionId): React.ComponentType | null => {
const upselling = useUpsellingService();

View file

@ -48,7 +48,7 @@ import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { of } from 'rxjs';
import { UpsellingService } from '../upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { NavigationProvider } from '@kbn/security-solution-navigation';

View file

@ -21,7 +21,7 @@ import {
} from './links';
import { createCapabilities } from './test_utils';
import { hasCapabilities } from '../lib/capabilities';
import { UpsellingService } from '../lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import React from 'react';
const defaultAppLinks: AppLinkItems = [

View file

@ -14,8 +14,8 @@ import type {
LinkCategory as GenericLinkCategory,
LinkCategories as GenericLinkCategories,
} from '@kbn/security-solution-navigation';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import type { UpsellingService } from '../lib/upsellings';
import type { RequiredCapabilities } from '../lib/capabilities';
/**

View file

@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
import { UpsellingService } from '../../lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { updateAppLinks } from '../../links';
import { links } from '../../links/app_links';
import { useShowTimeline } from './use_show_timeline';

View file

@ -87,14 +87,13 @@ const RiskDetailsTabBodyComponent: React.FC<
[entityName, riskEntity]
);
const { data, loading, refetch, inspect, isDeprecated, isModuleEnabled, isAuthorized } =
useRiskScore({
filterQuery,
onlyLatest: false,
riskEntity,
skip: !overTimeToggleStatus && !contributorsToggleStatus,
timerange,
});
const { data, loading, refetch, inspect, isDeprecated, isModuleEnabled } = useRiskScore({
filterQuery,
onlyLatest: false,
riskEntity,
skip: !overTimeToggleStatus && !contributorsToggleStatus,
timerange,
});
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
@ -136,10 +135,6 @@ const RiskDetailsTabBodyComponent: React.FC<
isDeprecated: isDeprecated && !loading,
};
if (!isAuthorized) {
return <>{'TODO: Add RiskScore Upsell'}</>;
}
if (riskScoreEngineStatus?.isUpdateAvailable) {
return <RiskScoreUpdatePanel />;
}

View file

@ -66,6 +66,7 @@ import { LandingPageComponent } from '../../../../common/components/landing_page
import { AlertCountByRuleByStatus } from '../../../../common/components/alert_count_by_status';
import { useLicense } from '../../../../common/hooks/use_license';
import { ResponderActionButton } from '../../../../detections/components/endpoint_responder/responder_action_button';
import { useHasSecurityCapability } from '../../../../helper_hooks';
const ES_HOST_FIELD = 'host.name';
const HostOverviewManage = manageQuery(HostOverview);
@ -152,7 +153,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
dispatch(setHostDetailsTablesActivePageToZero());
}, [dispatch, detailName]);
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges();
const canReadAlerts = hasKibanaREAD && hasIndexRead;
@ -252,7 +253,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
<TabNavigation
navTabs={navTabsHostDetails({
hasMlUserPermissions: hasMlUserPermissions(capabilities),
isRiskyHostsEnabled: isPlatinumOrTrialLicense,
isRiskyHostsEnabled: hasEntityAnalyticsCapability,
hostName: detailName,
isEnterprise: isEnterprisePlus,
})}

View file

@ -56,6 +56,7 @@ import { ID } from '../containers/hosts';
import { LandingPageComponent } from '../../../common/components/landing_page';
import { fieldNameExistsFilter } from '../../../common/components/visualization_actions/utils';
import { useLicense } from '../../../common/hooks/use_license';
import { useHasSecurityCapability } from '../../../helper_hooks';
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
@ -156,6 +157,7 @@ const HostsComponent = () => {
});
const isEnterprisePlus = useLicense().isEnterprise();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const onSkipFocusBeforeEventsTable = useCallback(() => {
containerElement.current
@ -215,7 +217,7 @@ const HostsComponent = () => {
<TabNavigation
navTabs={navTabsHosts({
hasMlUserPermissions: hasMlUserPermissions(capabilities),
isRiskyHostsEnabled: capabilities.isPlatinumOrTrialLicense,
isRiskyHostsEnabled: hasEntityAnalyticsCapability,
isEnterprise: isEnterprisePlus,
})}
/>

View file

@ -74,7 +74,6 @@ export const HostRiskScoreQueryTabBody = ({
isModuleEnabled,
loading,
refetch,
isAuthorized,
totalCount,
} = useRiskScore({
filterQuery,
@ -96,10 +95,6 @@ export const HostRiskScoreQueryTabBody = ({
isDeprecated: isDeprecated && !loading,
};
if (!isAuthorized) {
return <>{'TODO: Add RiskScore Upsell'}</>;
}
if (riskScoreEngineStatus?.isUpdateAvailable) {
return <RiskScoreUpdatePanel />;
}

View file

@ -64,6 +64,7 @@ import { UsersType } from '../../store/model';
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
import { LandingPageComponent } from '../../../../common/components/landing_page';
import { useHasSecurityCapability } from '../../../../helper_hooks';
const QUERY_ID = 'UsersDetailsQueryId';
const ES_USER_FIELD = 'user.name';
@ -73,7 +74,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
usersDetailsPagePath,
}) => {
const dispatch = useDispatch();
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.hostsPageEvents) ?? timelineDefaults).graphEventId
@ -241,7 +242,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
navTabs={navTabsUsersDetails(
detailName,
hasMlUserPermissions(capabilities),
isPlatinumOrTrialLicense
hasEntityAnalyticsCapability
)}
/>
<EuiSpacer />

View file

@ -75,7 +75,6 @@ export const UserRiskScoreQueryTabBody = ({
loading,
refetch,
totalCount,
isAuthorized,
} = useRiskScore({
filterQuery,
pagination,
@ -96,10 +95,6 @@ export const UserRiskScoreQueryTabBody = ({
isDeprecated: isDeprecated && !loading,
};
if (!isAuthorized) {
return <>{'TODO: Add RiskScore Upsell'}</>;
}
if (riskScoreEngineStatus?.isUpdateAvailable) {
return <RiskScoreUpdatePanel />;
}

View file

@ -52,6 +52,7 @@ import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { LandingPageComponent } from '../../../common/components/landing_page';
import { userNameExistsFilter } from './details/helpers';
import { useHasSecurityCapability } from '../../../helper_hooks';
const ID = 'UsersQueryId';
@ -175,10 +176,10 @@ const UsersComponent = () => {
);
const capabilities = useMlCapabilities();
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const navTabs = useMemo(
() => navTabsUsers(hasMlUserPermissions(capabilities), isPlatinumOrTrialLicense),
[capabilities, isPlatinumOrTrialLicense]
() => navTabsUsers(hasMlUserPermissions(capabilities), hasEntityAnalyticsCapability),
[capabilities, hasEntityAnalyticsCapability]
);
return (

View file

@ -12,13 +12,6 @@ import type { PluginSetup, PluginStart } from './types';
export type { TimelineModel } from './timelines/store/timeline/model';
export type { LinkItem } from './common/links';
export type {
UpsellingService,
PageUpsellings,
SectionUpsellings,
UpsellingSectionId,
} from './common/lib/upsellings';
export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context);
export type { PluginSetup, PluginStart };

View file

@ -10,6 +10,7 @@ import React, { memo } from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import type { Store } from 'redux';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import { UpsellingProvider } from '../../../../../../../common/components/upselling_provider';
import { UserPrivilegesProvider } from '../../../../../../../common/components/user_privileges/user_privileges_context';
import type { SecuritySolutionQueryClient } from '../../../../../../../common/containers/query_client/query_client_provider';
@ -18,7 +19,6 @@ import { SecuritySolutionStartDependenciesContext } from '../../../../../../../c
import { CurrentLicense } from '../../../../../../../common/components/current_license';
import type { StartPlugins } from '../../../../../../../types';
import { useKibana } from '../../../../../../../common/lib/kibana';
import type { UpsellingService } from '../../../../../../..';
export type RenderContextProvidersProps = PropsWithChildren<{
store: Store;

View file

@ -6,8 +6,8 @@
*/
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { StartPlugins } from '../../../../../types';
import type { UpsellingService } from '../../../../..';
export interface FleetUiExtensionGetterOptions {
coreStart: CoreStart;

View file

@ -12,7 +12,7 @@ import { createAppRootMockRenderer } from '../../../../../common/mock/endpoint';
import type { PolicySettingsFormProps } from './policy_settings_form';
import { PolicySettingsForm } from './policy_settings_form';
import { FleetPackagePolicyGenerator } from '../../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import type { UpsellingService } from '../../../../../common/lib/upsellings';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
jest.mock('../../../../../common/hooks/use_license');

View file

@ -6,9 +6,9 @@
*/
import { BehaviorSubject } from 'rxjs';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { BreadcrumbsNav } from './common/breadcrumbs';
import type { NavigationLink } from './common/links/types';
import { UpsellingService } from './common/lib/upsellings';
import type { PluginStart, PluginSetup } from './types';
const setupMock = (): PluginSetup => ({

View file

@ -88,7 +88,7 @@ export const entityAnalyticsLinks: LinkItem = {
'Entity analytics, anomalies, and threats to narrow down the monitoring surface area.',
}),
path: ENTITY_ANALYTICS_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
capabilities: [`${SERVER_APP_ID}.entity-analytics`],
isBeta: false,
licenseType: 'platinum',
globalSearchKeywords: [ENTITY_ANALYTICS],

View file

@ -7,7 +7,7 @@
import { BehaviorSubject } from 'rxjs';
import type { RouteProps } from 'react-router-dom';
import { UpsellingService } from './common/lib/upsellings';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { ContractStartServices, PluginSetup, PluginStart } from './types';
import type { AppLinksSwitcher } from './common/links';
import { navLinks$ } from './common/links/nav_links';

View file

@ -54,6 +54,7 @@ import type { RouteProps } from 'react-router-dom';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { ResolverPluginSetup } from './resolver/types';
import type { Inspect } from '../common/search_strategy';
import type { Detections } from './detections';
@ -74,7 +75,6 @@ import type { EntityAnalytics } from './entity_analytics';
import type { TelemetryClientStart } from './common/lib/telemetry';
import type { Dashboards } from './dashboards';
import type { UpsellingService } from './common/lib/upsellings';
import type { BreadcrumbsNav } from './common/breadcrumbs/types';
import type { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';

View file

@ -163,6 +163,7 @@
"@kbn/cloud-chat-plugin",
"@kbn/alerts-ui-shared",
"@kbn/security-solution-navigation",
"@kbn/security-solution-upselling",
"@kbn/discover-plugin",
"@kbn/data-view-editor-plugin",
"@kbn/navigation-plugin",

View file

@ -1,34 +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 { i18n } from '@kbn/i18n';
export const PLATINUM = i18n.translate('xpack.securitySolutionEss.paywall.platinum', {
defaultMessage: 'Platinum',
});
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.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

@ -13,14 +13,15 @@ import type {
SectionUpsellings,
UpsellingMessageId,
UpsellingSectionId,
} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types';
} from '@kbn/security-solution-upselling/service/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 React, { lazy } from 'react';
import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
const EntityAnalyticsUpsellingLazy = lazy(() => import('./pages/entity_analytics_upselling'));
const EntityAnalyticsUpsellingLazy = lazy(
() => import('@kbn/security-solution-upselling/pages/entity_analytics')
);
interface UpsellingsConfig {
minimumLicenseRequired: LicenseType;
@ -42,7 +43,7 @@ export const registerUpsellings = (
license: ILicense,
services: Services
) => {
const upsellingPagesToRegister = upsellingPages.reduce<PageUpsellings>(
const upsellingPagesToRegister = upsellingPages(services).reduce<PageUpsellings>(
(pageUpsellings, { pageName, minimumLicenseRequired, component }) => {
if (!license.hasAtLeast(minimumLicenseRequired)) {
pageUpsellings[pageName] = withServicesProvider(component, services);
@ -78,12 +79,19 @@ export const registerUpsellings = (
};
// Upsellings for entire pages, linked to a SecurityPageName
export const upsellingPages: UpsellingPages = [
export const upsellingPages: (services: Services) => UpsellingPages = (services) => [
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
{
pageName: SecurityPageName.entityAnalytics,
minimumLicenseRequired: 'platinum',
component: EntityAnalyticsUpsellingLazy,
component: () => (
<EntityAnalyticsUpsellingLazy
requiredLicense="Platinum"
subscriptionUrl={services.application.getUrlForApp('management', {
path: 'stack/license_management',
})}
/>
),
},
];
@ -97,6 +105,6 @@ export const upsellingMessages: UpsellingMessages = [
{
id: 'investigation_guide',
minimumLicenseRequired: 'platinum',
message: UPGRADE_INVESTIGATION_GUIDE('platinum'),
message: UPGRADE_INVESTIGATION_GUIDE('Platinum'),
},
];

View file

@ -21,6 +21,6 @@
"@kbn/kibana-react-plugin",
"@kbn/security-solution-navigation",
"@kbn/licensing-plugin",
"@kbn/shared-ux-page-kibana-template",
"@kbn/security-solution-upselling",
]
}

View file

@ -54,7 +54,8 @@ export class SecuritySolutionServerlessPlugin
const services = createServices(core, startDeps);
registerUpsellings(securitySolution.getUpselling(), this.config.productTypes);
registerUpsellings(securitySolution.getUpselling(), this.config.productTypes, services);
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes));
configureNavigation(services, this.config);

View file

@ -26,3 +26,7 @@ export const ThreatIntelligencePaywallLazy = withSuspenseUpsell(
export const OsqueryResponseActionsUpsellingSectionLazy = withSuspenseUpsell(
lazy(() => import('./pages/osquery_automated_response_actions'))
);
export const EntityAnalyticsUpsellingLazy = withSuspenseUpsell(
lazy(() => import('@kbn/security-solution-upselling/pages/entity_analytics'))
);

View file

@ -1,24 +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 type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
import { i18n } from '@kbn/i18n';
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
export const UPGRADE_INVESTIGATION_GUIDE = (productTypeRequired: string) =>
i18n.translate('xpack.securitySolutionServerless.markdown.insight.upsell', {
defaultMessage:
'Upgrade to {productTypeRequired} to make use of insights in investigation guides',
values: {
productTypeRequired,
},
});
export const investigationGuideUpselling = (requiredPLI: AppFeatureKey): string => {
const productTypeRequired = getProductTypeByPLI(requiredPLI);
return productTypeRequired ? UPGRADE_INVESTIGATION_GUIDE(productTypeRequired) : '';
};

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 { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
export const GenericUpsellingPage: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
function GenericUpsellingPage({ requiredPLI }) {
const productTypeRequired = getProductTypeByPLI(requiredPLI);
return (
<EuiEmptyPrompt
iconType="logoSecurity"
title={<>{'This is a testing component for a Serverless upselling prompt.'}</>}
body={
<>
{'Get'} <EuiLink href="#">{productTypeRequired}</EuiLink> {'to enable this feature'}
<br />
<br />
<iframe
title="money"
src="https://giphy.com/embed/px8O7NANzzaqk"
width="480"
height="283"
frameBorder="0"
className="giphy-embed"
allowFullScreen
/>
</>
}
/>
);
}
);
// eslint-disable-next-line import/no-default-export
export { GenericUpsellingPage as default };

View file

@ -14,16 +14,20 @@ import type {
import type {
MessageUpsellings,
UpsellingMessageId,
} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types';
} from '@kbn/security-solution-upselling/service/types';
import React from 'react';
import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages';
import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
import { getProductAppFeatures } from '../../common/pli/pli_features';
import { investigationGuideUpselling } from './messages/investigation_guide_upselling';
import {
OsqueryResponseActionsUpsellingSectionLazy,
ThreatIntelligencePaywallLazy,
EntityAnalyticsUpsellingLazy,
} from './lazy_upselling';
import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
interface UpsellingsConfig {
pli: AppFeatureKey;
@ -42,14 +46,15 @@ type UpsellingMessages = UpsellingsMessageConfig[];
export const registerUpsellings = (
upselling: UpsellingService,
productTypes: SecurityProductTypes
productTypes: SecurityProductTypes,
services: Services
) => {
const enabledPLIsSet = new Set(getProductAppFeatures(productTypes));
const upsellingPagesToRegister = upsellingPages.reduce<PageUpsellings>(
(pageUpsellings, { pageName, pli, component }) => {
if (!enabledPLIsSet.has(pli)) {
pageUpsellings[pageName] = component;
pageUpsellings[pageName] = withServicesProvider(component, services);
}
return pageUpsellings;
},
@ -84,6 +89,15 @@ export const registerUpsellings = (
// 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,
pli: AppFeatureKey.advancedInsights,
component: () => (
<EntityAnalyticsUpsellingLazy
requiredProduct={getProductTypeByPLI(AppFeatureKey.advancedInsights) ?? undefined}
/>
),
},
{
pageName: SecurityPageName.threatIntelligence,
pli: AppFeatureKey.threatIntelligence,
@ -117,6 +131,8 @@ export const upsellingMessages: UpsellingMessages = [
{
id: 'investigation_guide',
pli: AppFeatureKey.investigationGuide,
message: investigationGuideUpselling(AppFeatureKey.investigationGuide),
message: UPGRADE_INVESTIGATION_GUIDE(
getProductTypeByPLI(AppFeatureKey.investigationGuide) ?? ''
),
},
];

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 { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
import type { AppFeatureKey } from '@kbn/security-solution-plugin/common';
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
export const GenericUpsellingSection: React.FC<{ requiredPLI: AppFeatureKey }> = React.memo(
function GenericUpsellingSection({ requiredPLI }) {
const productTypeRequired = getProductTypeByPLI(requiredPLI);
return (
<EuiEmptyPrompt
iconType="logoSecurity"
title={<>{'This is a testing component for a Serverless upselling prompt.'}</>}
body={
<>
{'Get'} <EuiLink href="#">{productTypeRequired}</EuiLink> {'to enable this feature'}
<br />
<br />
<iframe
title="money"
src="https://giphy.com/embed/px8O7NANzzaqk"
width="480"
height="283"
frameBorder="0"
className="giphy-embed"
allowFullScreen
/>
</>
}
/>
);
}
);
// eslint-disable-next-line import/no-default-export
export { GenericUpsellingSection as default };

View file

@ -10,6 +10,8 @@
"public/**/*.tsx",
"server/**/*.ts",
"../../../typings/**/*"
,
"../../packages/security-solution/upselling/sections/generic_upselling_section.tsx"
],
"exclude": ["target/**/*"],
"kbn_references": [
@ -23,6 +25,7 @@
"@kbn/shared-ux-page-solution-nav",
"@kbn/security-solution-side-nav",
"@kbn/security-solution-navigation",
"@kbn/security-solution-upselling",
"@kbn/default-nav-ml",
"@kbn/default-nav-devtools",
"@kbn/kibana-react-plugin",

View file

@ -5310,6 +5310,10 @@
version "0.0.0"
uid ""
"@kbn/security-solution-upselling@link:x-pack/packages/security-solution/upselling":
version "0.0.0"
uid ""
"@kbn/security-test-endpoints-plugin@link:x-pack/test/security_functional/plugins/test_endpoints":
version "0.0.0"
uid ""