[SecuritySolution] Show upselling component for entity risk score tab (#183517)

## Summary

### Before
Serverless: The tab is hidden

![Screenshot 2024-05-16 at 15 57
36](bd270106-70a7-452e-9b22-5a3960a615a3)

ESS: We displayed the unauthorized banner

![Screenshot 2024-05-16 at 15 38
24](164a16ad-08d4-49b6-b592-ee9ae91cd196)

### After
Display the upsell banner
![Screenshot 2024-05-16 at 14 46
11](a64593aa-789c-4db4-8747-68b5d5e51435)


## How to test it

* For every license and tier 
  * Go to host/user page inside, explore menu item
  * Verify what is displayed inside the risk score tab


* Run security serverless with "Security Analytics Essentials"  tier
  * You should see the upsell component
* Run security serverless with "Security Analytics Complete"  tier
  * You should NOT see the upsell component
* Run kibana ESS with platinum license
  * You should NOT see the upsell component
* Run kibana ESS with basic license
  * You should see the upsell component


### Checklist

Delete any items that are not applicable to this PR.


- [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
This commit is contained in:
Pablo Machado 2024-05-22 16:05:40 +02:00 committed by GitHub
parent 8189e1a753
commit 9d70accb2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 368 additions and 306 deletions

View file

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

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { render } from '@testing-library/react';
import EntityAnalyticsUpsellingComponent from './entity_analytics';
import { EntityAnalyticsUpsellingPage } from './entity_analytics';
jest.mock('@kbn/security-solution-navigation', () => {
const original = jest.requireActual('@kbn/security-solution-navigation');
@ -21,54 +21,33 @@ jest.mock('@kbn/security-solution-navigation', () => {
describe('EntityAnalyticsUpselling', () => {
it('should render', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
<EntityAnalyticsUpsellingPage
upgradeMessage="test upgrade message"
upgradeToLabel="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', () => {
it('should show upgrade label badge', () => {
const { getByText } = render(
<EntityAnalyticsUpsellingComponent
requiredProduct="TEST PRODUCT"
requiredLicense="TEST LICENSE"
<EntityAnalyticsUpsellingPage
upgradeToLabel="TEST PRODUCT"
upgradeMessage="test upgrade message"
/>
);
expect(getByText('TEST PRODUCT')).toBeInTheDocument();
});
it('should show license message when requiredLicense is provided', () => {
it('should show license message', () => {
const { getByTestId } = render(
<EntityAnalyticsUpsellingComponent requiredLicense="TEST LICENSE" />
<EntityAnalyticsUpsellingPage
upgradeToLabel="TEST PRODUCT"
upgradeMessage="test upgrade message"
/>
);
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();
expect(getByTestId('paywallCardDescription')).toHaveTextContent('test upgrade message');
});
});

View file

@ -5,25 +5,15 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import {
EuiCard,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiTextColor,
EuiImage,
EuiPageHeader,
EuiSpacer,
} from '@elastic/eui';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, 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 '../images/entity_paywall.png';
import { EntityAnalyticsUpsellingSection } from '../sections/entity_analytics';
const PaywallDiv = styled.div`
max-width: 75%;
@ -37,81 +27,27 @@ const PaywallDiv = styled.div`
padding: 0 15%;
}
`;
const StyledEuiCard = styled(EuiCard)`
span.euiTitle {
max-width: 540px;
display: block;
margin: 0 auto;
}
`;
const EntityAnalyticsUpsellingComponent = ({
requiredLicense,
requiredProduct,
subscriptionUrl,
upgradeMessage,
upgradeHref,
upgradeToLabel,
}: {
requiredLicense?: string;
requiredProduct?: string;
subscriptionUrl?: string;
upgradeMessage: string;
upgradeToLabel: string;
upgradeHref?: 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>
<EuiPageHeader pageTitle={i18n.ENTITY_ANALYTICS_TITLE} />
<EuiSpacer size="xl" />
<PaywallDiv>
<StyledEuiCard
betaBadgeProps={{ label: requiredProductOrLicense }}
icon={<EuiIcon size="xl" type="lock" />}
display="subdued"
title={
<h3>
<strong>{i18n.ENTITY_ANALYTICS_LICENSE_DESC}</strong>
</h3>
}
description={false}
paddingSize="xl"
>
<EuiFlexGroup
data-test-subj="paywallCardDescription"
className="paywallCardDescription"
direction="column"
gutterSize="none"
>
<EuiText>
<EuiFlexItem>
<p>
<EuiTextColor color="subdued">{upgradeMessage}</EuiTextColor>
</p>
</EuiFlexItem>
<EuiFlexItem>
{subscriptionUrl && (
<div>
<EuiButton onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON(requiredProductOrLicense)}
</EuiButton>
</div>
)}
</EuiFlexItem>
</EuiText>
</EuiFlexGroup>
</StyledEuiCard>
<EntityAnalyticsUpsellingSection
upgradeMessage={upgradeMessage}
upgradeHref={upgradeHref}
upgradeToLabel={upgradeToLabel}
/>
<EuiFlexGroup>
<EuiFlexItem>
<EuiImage alt={upgradeMessage} src={paywallPng} size="fullWidth" />
@ -125,5 +61,4 @@ const EntityAnalyticsUpsellingComponent = ({
EntityAnalyticsUpsellingComponent.displayName = 'EntityAnalyticsUpsellingComponent';
// eslint-disable-next-line import/no-default-export
export default React.memo(EntityAnalyticsUpsellingComponent);
export const EntityAnalyticsUpsellingPage = React.memo(EntityAnalyticsUpsellingComponent);

View file

@ -7,23 +7,6 @@
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}',

View file

@ -0,0 +1,88 @@
/*
* 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,
EuiTextColor,
EuiButton,
} from '@elastic/eui';
import styled from '@emotion/styled';
import { useNavigation } from '@kbn/security-solution-navigation';
import * as i18n from '../pages/translations';
const StyledEuiCard = styled(EuiCard)`
span.euiTitle {
max-width: 540px;
display: block;
margin: 0 auto;
}
`;
export const EntityAnalyticsUpsellingSection = memo(
({
upgradeMessage,
upgradeHref,
upgradeToLabel,
}: {
upgradeMessage: string;
upgradeToLabel: string;
upgradeHref?: string;
}) => {
const { navigateTo } = useNavigation();
const goToSubscription = useCallback(() => {
navigateTo({ url: upgradeHref });
}, [navigateTo, upgradeHref]);
return (
<StyledEuiCard
betaBadgeProps={{ label: upgradeToLabel }}
icon={<EuiIcon size="xl" type="lock" />}
display="subdued"
title={
<h3>
<strong>{i18n.ENTITY_ANALYTICS_LICENSE_DESC}</strong>
</h3>
}
description={false}
paddingSize="xl"
>
<EuiFlexGroup
data-test-subj="paywallCardDescription"
className="paywallCardDescription"
direction="column"
gutterSize="none"
>
<EuiText>
<EuiFlexItem>
<p>
<EuiTextColor color="subdued">{upgradeMessage}</EuiTextColor>
</p>
</EuiFlexItem>
<EuiFlexItem>
{upgradeHref && (
<div>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click*/}
<EuiButton href={upgradeHref} onClick={goToSubscription} fill>
{i18n.UPGRADE_BUTTON(upgradeToLabel)}
</EuiButton>
</div>
)}
</EuiFlexItem>
</EuiText>
</EuiFlexGroup>
</StyledEuiCard>
);
}
);
EntityAnalyticsUpsellingSection.displayName = 'EntityAnalyticsUpsellingSection';

View file

@ -41,14 +41,12 @@ const hostName = 'siem-window';
describe('Table Navigation', () => {
const mockHasMlUserPermissions = true;
const mockRiskyHostEnabled = true;
mockUseRouteSpy.mockReturnValue([{ tabName: HostsTableType.authentications }]);
const mockProps: TabNavigationProps = {
navTabs: navTabsHostDetails({
hostName,
hasMlUserPermissions: mockHasMlUserPermissions,
isRiskyHostsEnabled: mockRiskyHostEnabled,
}),
};

View file

@ -9,6 +9,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useUpsellingComponent } from '../../../common/hooks/use_upselling';
import { RISKY_HOSTS_DASHBOARD_TITLE, RISKY_USERS_DASHBOARD_TITLE } from '../risk_score/constants';
import { EnableRiskScore } from '../enable_risk_score';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
@ -130,6 +131,11 @@ const RiskDetailsTabBodyComponent: React.FC<
const privileges = useMissingRiskEnginePrivileges();
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
if (RiskScoreUpsell) {
return <RiskScoreUpsell />;
}
if (!privileges.isLoading && !privileges.hasAllRequiredPrivileges) {
return (
<EuiPanel hasBorder>

View file

@ -26,6 +26,7 @@ import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status';
import { RiskScoreUpdatePanel } from './risk_score_update_panel';
import { useMissingRiskEnginePrivileges } from '../hooks/use_missing_risk_engine_privileges';
import { RiskEnginePrivilegesCallOut } from './risk_engine_privileges_callout';
import { useUpsellingComponent } from '../../common/hooks/use_upselling';
const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable);
@ -97,6 +98,11 @@ export const UserRiskScoreQueryTabBody = ({
isDeprecated: isDeprecated && !loading,
};
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
if (RiskScoreUpsell) {
return <RiskScoreUpsell />;
}
if (!privileges.isLoading && !privileges.hasAllRequiredPrivileges) {
return (
<EuiPanel hasBorder>

View file

@ -78,7 +78,6 @@ import { EmptyPrompt } from '../../../../common/components/empty_prompt';
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';
import { useRefetchOverviewPageRiskScore } from '../../../../entity_analytics/api/hooks/use_refetch_overview_page_risk_score';
const ES_HOST_FIELD = 'host.name';
@ -167,8 +166,6 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
dispatch(setHostDetailsTablesActivePageToZero());
}, [dispatch, detailName]);
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges();
const canReadAlerts = hasKibanaREAD && hasIndexRead;
@ -296,7 +293,6 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
<TabNavigation
navTabs={navTabsHostDetails({
hasMlUserPermissions: hasMlUserPermissions(capabilities),
isRiskyHostsEnabled: hasEntityAnalyticsCapability,
hostName: detailName,
isEnterprise: isEnterprisePlus,
})}

View file

@ -14,37 +14,8 @@ describe('navTabsHostDetails', () => {
test('it should skip anomalies tab if without mlUserPermission', () => {
const tabs = navTabsHostDetails({
hasMlUserPermissions: false,
isRiskyHostsEnabled: false,
hostName: mockHostName,
});
expect(tabs).toHaveProperty(HostsTableType.authentications);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
expect(tabs).not.toHaveProperty(HostsTableType.anomalies);
expect(tabs).toHaveProperty(HostsTableType.events);
expect(tabs).not.toHaveProperty(HostsTableType.risk);
});
test('it should display anomalies tab if with mlUserPermission', () => {
const tabs = navTabsHostDetails({
hasMlUserPermissions: true,
isRiskyHostsEnabled: false,
hostName: mockHostName,
});
expect(tabs).toHaveProperty(HostsTableType.authentications);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
expect(tabs).toHaveProperty(HostsTableType.anomalies);
expect(tabs).toHaveProperty(HostsTableType.events);
expect(tabs).not.toHaveProperty(HostsTableType.risk);
});
test('it should display risky hosts tab if when risky hosts is enabled', () => {
const tabs = navTabsHostDetails({
hasMlUserPermissions: false,
isRiskyHostsEnabled: true,
hostName: mockHostName,
});
expect(tabs).toHaveProperty(HostsTableType.authentications);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
expect(tabs).not.toHaveProperty(HostsTableType.anomalies);
@ -52,10 +23,22 @@ describe('navTabsHostDetails', () => {
expect(tabs).toHaveProperty(HostsTableType.risk);
});
test('it should display anomalies tab if with mlUserPermission', () => {
const tabs = navTabsHostDetails({
hasMlUserPermissions: true,
hostName: mockHostName,
});
expect(tabs).toHaveProperty(HostsTableType.authentications);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
expect(tabs).toHaveProperty(HostsTableType.anomalies);
expect(tabs).toHaveProperty(HostsTableType.events);
expect(tabs).toHaveProperty(HostsTableType.risk);
});
test('it should display sessions tab when users are on Enterprise and above license', () => {
const tabs = navTabsHostDetails({
hasMlUserPermissions: false,
isRiskyHostsEnabled: true,
hostName: mockHostName,
isEnterprise: true,
});
@ -70,7 +53,6 @@ describe('navTabsHostDetails', () => {
test('it should not display sessions tab when users are not on Enterprise and above license', () => {
const tabs = navTabsHostDetails({
hasMlUserPermissions: false,
isRiskyHostsEnabled: true,
hostName: mockHostName,
isEnterprise: false,
});

View file

@ -16,13 +16,11 @@ const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) =>
export const navTabsHostDetails = ({
hasMlUserPermissions,
isRiskyHostsEnabled,
hostName,
isEnterprise,
}: {
hostName: string;
hasMlUserPermissions: boolean;
isRiskyHostsEnabled: boolean;
isEnterprise?: boolean;
}): HostDetailsNavTab => {
const hiddenTabs = [];
@ -71,10 +69,6 @@ export const navTabsHostDetails = ({
hiddenTabs.push(HostsTableType.anomalies);
}
if (!isRiskyHostsEnabled) {
hiddenTabs.push(HostsTableType.risk);
}
if (!isEnterprise) {
hiddenTabs.push(HostsTableType.sessions);
}

View file

@ -53,7 +53,6 @@ import { ID } from '../containers/hosts';
import { EmptyPrompt } from '../../../common/components/empty_prompt';
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.
@ -138,7 +137,6 @@ const HostsComponent = () => {
});
const isEnterprisePlus = useLicense().isEnterprise();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const onSkipFocusBeforeEventsTable = useCallback(() => {
containerElement.current
@ -190,7 +188,6 @@ const HostsComponent = () => {
<TabNavigation
navTabs={navTabsHosts({
hasMlUserPermissions: hasMlUserPermissions(capabilities),
isRiskyHostsEnabled: hasEntityAnalyticsCapability,
isEnterprise: isEnterprisePlus,
})}
/>

View file

@ -12,7 +12,6 @@ describe('navTabsHosts', () => {
test('it should skip anomalies tab if without mlUserPermission', () => {
const tabs = navTabsHosts({
hasMlUserPermissions: false,
isRiskyHostsEnabled: false,
});
expect(tabs).toHaveProperty(HostsTableType.hosts);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
@ -23,7 +22,6 @@ describe('navTabsHosts', () => {
test('it should display anomalies tab if with mlUserPermission', () => {
const tabs = navTabsHosts({
hasMlUserPermissions: true,
isRiskyHostsEnabled: false,
});
expect(tabs).toHaveProperty(HostsTableType.hosts);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
@ -31,21 +29,9 @@ describe('navTabsHosts', () => {
expect(tabs).toHaveProperty(HostsTableType.events);
});
test('it should skip risk tab if without hostRisk', () => {
test('it should display risk tab', () => {
const tabs = navTabsHosts({
hasMlUserPermissions: false,
isRiskyHostsEnabled: false,
});
expect(tabs).toHaveProperty(HostsTableType.hosts);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);
expect(tabs).not.toHaveProperty(HostsTableType.risk);
expect(tabs).toHaveProperty(HostsTableType.events);
});
test('it should display risk tab if with hostRisk', () => {
const tabs = navTabsHosts({
hasMlUserPermissions: false,
isRiskyHostsEnabled: true,
});
expect(tabs).toHaveProperty(HostsTableType.hosts);
expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses);

View file

@ -15,11 +15,9 @@ const getTabsOnHostsUrl = (tabName: HostsTableType) => `${HOSTS_PATH}/${tabName}
export const navTabsHosts = ({
hasMlUserPermissions,
isRiskyHostsEnabled,
isEnterprise,
}: {
hasMlUserPermissions: boolean;
isRiskyHostsEnabled: boolean;
isEnterprise?: boolean;
}): HostsNavTab => {
const hiddenTabs = [];
@ -67,10 +65,6 @@ export const navTabsHosts = ({
hiddenTabs.push(HostsTableType.anomalies);
}
if (!isRiskyHostsEnabled) {
hiddenTabs.push(HostsTableType.risk);
}
if (!isEnterprise) {
hiddenTabs.push(HostsTableType.sessions);
}

View file

@ -8,6 +8,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import { noop } from 'lodash/fp';
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
import { RiskEnginePrivilegesCallOut } from '../../../../entity_analytics/components/risk_engine_privileges_callout';
import { useMissingRiskEnginePrivileges } from '../../../../entity_analytics/hooks/use_missing_risk_engine_privileges';
import { HostRiskScoreQueryId } from '../../../../entity_analytics/common/utils';
@ -96,6 +97,12 @@ export const HostRiskScoreQueryTabBody = ({
isDeprecated: isDeprecated && !loading,
};
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
if (RiskScoreUpsell) {
return <RiskScoreUpsell />;
}
if (!privileges.isLoading && !privileges.hasAllRequiredPrivileges) {
return (
<EuiPanel hasBorder>

View file

@ -76,7 +76,6 @@ 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 { EmptyPrompt } from '../../../../common/components/empty_prompt';
import { useHasSecurityCapability } from '../../../../helper_hooks';
import { useRefetchOverviewPageRiskScore } from '../../../../entity_analytics/api/hooks/use_refetch_overview_page_risk_score';
const QUERY_ID = 'UsersDetailsQueryId';
@ -87,7 +86,6 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
usersDetailsPagePath,
}) => {
const dispatch = useDispatch();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const graphEventId = useShallowEqualSelector(
(state) => (getTable(state, TableId.hostsPageEvents) ?? timelineDefaults).graphEventId
@ -285,11 +283,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
)}
<TabNavigation
navTabs={navTabsUsersDetails(
detailName,
hasMlUserPermissions(capabilities),
hasEntityAnalyticsCapability
)}
navTabs={navTabsUsersDetails(detailName, hasMlUserPermissions(capabilities))}
/>
<EuiSpacer />
<UsersDetailsTabs

View file

@ -9,16 +9,9 @@ import { navTabsUsersDetails } from './nav_tabs';
describe('navTabsUsersDetails', () => {
test('it should not display anomalies tab if user has no ml permission', () => {
const tabs = navTabsUsersDetails('username', false, true);
const tabs = navTabsUsersDetails('username', false);
expect(tabs).not.toHaveProperty(UsersTableType.anomalies);
expect(tabs).toHaveProperty(UsersTableType.risk);
});
test('it should not display risk tab if isRiskyUserEnabled disabled', () => {
const tabs = navTabsUsersDetails('username', true, false);
// expect(tabs).toHaveProperty(UsersTableType.allUsers);
expect(tabs).toHaveProperty(UsersTableType.anomalies);
expect(tabs).not.toHaveProperty(UsersTableType.risk);
});
});

View file

@ -16,8 +16,7 @@ const getTabsOnUsersDetailsUrl = (userName: string, tabName: UsersTableType) =>
export const navTabsUsersDetails = (
userName: string,
hasMlUserPermissions: boolean,
isRiskyUserEnabled: boolean
hasMlUserPermissions: boolean
): UsersDetailsNavTab => {
const hiddenTabs = [];
@ -52,9 +51,5 @@ export const navTabsUsersDetails = (
hiddenTabs.push(UsersTableType.anomalies);
}
if (!isRiskyUserEnabled) {
hiddenTabs.push(UsersTableType.risk);
}
return omit(hiddenTabs, userDetailsNavTabs);
};

View file

@ -10,23 +10,16 @@ import { navTabsUsers } from './nav_tabs';
describe('navTabsUsers', () => {
test('it should display all tabs', () => {
const tabs = navTabsUsers(true, true);
const tabs = navTabsUsers(true);
expect(tabs).toHaveProperty(UsersTableType.allUsers);
expect(tabs).toHaveProperty(UsersTableType.anomalies);
expect(tabs).toHaveProperty(UsersTableType.risk);
});
test('it should not display anomalies tab if user has no ml permission', () => {
const tabs = navTabsUsers(false, true);
const tabs = navTabsUsers(false);
expect(tabs).toHaveProperty(UsersTableType.allUsers);
expect(tabs).not.toHaveProperty(UsersTableType.anomalies);
expect(tabs).toHaveProperty(UsersTableType.risk);
});
test('it should not display risk tab if isRiskyUserEnabled disabled', () => {
const tabs = navTabsUsers(true, false);
expect(tabs).toHaveProperty(UsersTableType.allUsers);
expect(tabs).toHaveProperty(UsersTableType.anomalies);
expect(tabs).not.toHaveProperty(UsersTableType.risk);
});
});

View file

@ -13,10 +13,7 @@ import { USERS_PATH } from '../../../../common/constants';
const getTabsOnUsersUrl = (tabName: UsersTableType) => `${USERS_PATH}/${tabName}`;
export const navTabsUsers = (
hasMlUserPermissions: boolean,
isRiskyUserEnabled: boolean
): UsersNavTab => {
export const navTabsUsers = (hasMlUserPermissions: boolean): UsersNavTab => {
const hiddenTabs = [];
const userNavTabs = {
@ -56,9 +53,5 @@ export const navTabsUsers = (
hiddenTabs.push(UsersTableType.anomalies);
}
if (!isRiskyUserEnabled) {
hiddenTabs.push(UsersTableType.risk);
}
return omit(hiddenTabs, userNavTabs);
};

View file

@ -49,7 +49,6 @@ import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { EmptyPrompt } from '../../../common/components/empty_prompt';
import { userNameExistsFilter } from './details/helpers';
import { useHasSecurityCapability } from '../../../helper_hooks';
const ID = 'UsersQueryId';
@ -156,11 +155,7 @@ const UsersComponent = () => {
);
const capabilities = useMlCapabilities();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const navTabs = useMemo(
() => navTabsUsers(hasMlUserPermissions(capabilities), hasEntityAnalyticsCapability),
[capabilities, hasEntityAnalyticsCapability]
);
const navTabs = useMemo(() => navTabsUsers(hasMlUserPermissions(capabilities)), [capabilities]);
return (
<>

View file

@ -0,0 +1,26 @@
/*
* 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 { lazy } from 'react';
import { withSuspenseUpsell } from '@kbn/security-solution-upselling/helpers';
export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell(
lazy(() =>
import('./sections/entity_analytics_upselling').then(
({ EntityAnalyticsUpsellingSectionESS }) => ({
default: EntityAnalyticsUpsellingSectionESS,
})
)
)
);
export const EntityAnalyticsUpsellingPageLazy = lazy(() =>
import('./pages/entity_analytics_upselling').then(({ EntityAnalyticsUpsellingPageESS }) => ({
default: EntityAnalyticsUpsellingPageESS,
}))
);

View file

@ -0,0 +1,26 @@
/*
* 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 { EntityAnalyticsUpsellingPage } from '@kbn/security-solution-upselling/pages/entity_analytics';
import { useKibana } from '../../common/services';
import * as i18n from '../translations';
export const EntityAnalyticsUpsellingPageESS = () => {
const { services } = useKibana();
const requiredLicense = 'Platinum';
return (
<EntityAnalyticsUpsellingPage
upgradeMessage={i18n.UPGRADE_LICENSE_MESSAGE(requiredLicense ?? '')}
upgradeHref={services.application.getUrlForApp('management', {
path: 'stack/license_management',
})}
upgradeToLabel={requiredLicense}
/>
);
};

View file

@ -5,7 +5,14 @@
* 2.0.
*/
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public';
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import {
ALERT_SUPPRESSION_RULE_DETAILS,
ALERT_SUPPRESSION_RULE_FORM,
UPGRADE_ALERT_ASSIGNMENTS,
UPGRADE_INVESTIGATION_GUIDE,
} from '@kbn/security-solution-upselling/messages';
import type {
MessageUpsellings,
PageUpsellings,
@ -14,19 +21,13 @@ import type {
UpsellingSectionId,
UpsellingService,
} from '@kbn/security-solution-upselling/service';
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public';
import React, { lazy } from 'react';
import {
UPGRADE_ALERT_ASSIGNMENTS,
UPGRADE_INVESTIGATION_GUIDE,
ALERT_SUPPRESSION_RULE_FORM,
ALERT_SUPPRESSION_RULE_DETAILS,
} from '@kbn/security-solution-upselling/messages';
import type React from 'react';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
const EntityAnalyticsUpsellingLazy = lazy(
() => import('@kbn/security-solution-upselling/pages/entity_analytics')
);
import {
EntityAnalyticsUpsellingPageLazy,
EntityAnalyticsUpsellingSectionLazy,
} from './lazy_upselling';
interface UpsellingsConfig {
minimumLicenseRequired: LicenseType;
@ -48,7 +49,7 @@ export const registerUpsellings = (
license: ILicense,
services: Services
) => {
const upsellingPagesToRegister = upsellingPages(services).reduce<PageUpsellings>(
const upsellingPagesToRegister = upsellingPages.reduce<PageUpsellings>(
(pageUpsellings, { pageName, minimumLicenseRequired, component }) => {
if (!license.hasAtLeast(minimumLicenseRequired)) {
pageUpsellings[pageName] = withServicesProvider(component, services);
@ -61,7 +62,7 @@ export const registerUpsellings = (
const upsellingSectionsToRegister = upsellingSections.reduce<SectionUpsellings>(
(sectionUpsellings, { id, minimumLicenseRequired, component }) => {
if (!license.hasAtLeast(minimumLicenseRequired)) {
sectionUpsellings[id] = component;
sectionUpsellings[id] = withServicesProvider(component, services);
}
return sectionUpsellings;
},
@ -84,25 +85,23 @@ export const registerUpsellings = (
};
// Upsellings for entire pages, linked to a SecurityPageName
export const upsellingPages: (services: Services) => UpsellingPages = (services) => [
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
requiredLicense="Platinum"
subscriptionUrl={services.application.getUrlForApp('management', {
path: 'stack/license_management',
})}
/>
),
component: EntityAnalyticsUpsellingPageLazy,
},
];
// 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.
{
id: 'entity_analytics_panel',
minimumLicenseRequired: 'platinum',
component: EntityAnalyticsUpsellingSectionLazy,
},
];
// Upsellings for sections, linked by arbitrary ids

View file

@ -0,0 +1,25 @@
/*
* 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 { EntityAnalyticsUpsellingSection } from '@kbn/security-solution-upselling/sections/entity_analytics';
import { useKibana } from '../../common/services';
import * as i18n from '../translations';
export const EntityAnalyticsUpsellingSectionESS = () => {
const { services } = useKibana();
const requiredLicense = 'Platinum';
return (
<EntityAnalyticsUpsellingSection
upgradeMessage={i18n.UPGRADE_LICENSE_MESSAGE(requiredLicense ?? '')}
upgradeHref={services.application.getUrlForApp('management', {
path: 'stack/license_management',
})}
upgradeToLabel={requiredLicense}
/>
);
};

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_LICENSE_MESSAGE = (requiredLicense: string) =>
i18n.translate('xpack.securitySolutionEss.upselling.upgradeLicenseMessage', {
defaultMessage: 'This feature is available with {requiredLicense} or higher subscription',
values: {
requiredLicense,
},
});

View file

@ -5,19 +5,9 @@
* 2.0.
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { lazy } from 'react';
const withSuspenseUpsell = <T extends object = {}>(
Component: React.ComponentType<T>
): React.FC<T> =>
function WithSuspenseUpsell(props) {
return (
<Suspense fallback={<EuiLoadingSpinner size="s" />}>
<Component {...props} />
</Suspense>
);
};
import { withSuspenseUpsell } from '@kbn/security-solution-upselling/helpers';
export const ThreatIntelligencePaywallLazy = withSuspenseUpsell(
lazy(() => import('./pages/threat_intelligence_paywall'))
@ -31,6 +21,22 @@ export const EndpointExceptionsDetailsUpsellingLazy = withSuspenseUpsell(
lazy(() => import('./pages/endpoint_management/endpoint_exceptions_details'))
);
export const EntityAnalyticsUpsellingLazy = withSuspenseUpsell(
lazy(() => import('@kbn/security-solution-upselling/pages/entity_analytics'))
export const EntityAnalyticsUpsellingPageLazy = withSuspenseUpsell(
lazy(() =>
import('@kbn/security-solution-upselling/pages/entity_analytics').then(
({ EntityAnalyticsUpsellingPage }) => ({
default: EntityAnalyticsUpsellingPage,
})
)
)
);
export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell(
lazy(() =>
import('@kbn/security-solution-upselling/sections/entity_analytics').then(
({ EntityAnalyticsUpsellingSection }) => ({
default: EntityAnalyticsUpsellingSection,
})
)
)
);

View file

@ -4,7 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import {
UPGRADE_INVESTIGATION_GUIDE,
UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
} from '@kbn/security-solution-upselling/messages';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import type {
MessageUpsellings,
PageUpsellings,
@ -12,31 +19,26 @@ import type {
UpsellingMessageId,
UpsellingSectionId,
} from '@kbn/security-solution-upselling/service/types';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import React from 'react';
import type { SecurityProductTypes } from '../../common/config';
import { getProductProductFeatures } from '../../common/pli/pli_features';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
import {
UPGRADE_INVESTIGATION_GUIDE,
UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
} from '@kbn/security-solution-upselling/messages';
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
EndpointExceptionsDetailsUpsellingLazy,
EntityAnalyticsUpsellingPageLazy,
EntityAnalyticsUpsellingSectionLazy,
OsqueryResponseActionsUpsellingSectionLazy,
ThreatIntelligencePaywallLazy,
} from './lazy_upselling';
import {
EndpointAgentTamperProtectionLazy,
EndpointPolicyProtectionsLazy,
EndpointProtectionUpdatesLazy,
RuleDetailsEndpointExceptionsLazy,
} from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
import { getProductProductFeatures } from '../../common/pli/pli_features';
import {
EndpointExceptionsDetailsUpsellingLazy,
EntityAnalyticsUpsellingLazy,
OsqueryResponseActionsUpsellingSectionLazy,
ThreatIntelligencePaywallLazy,
} from './lazy_upselling';
import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
import * as i18n from './translations';
interface UpsellingsConfig {
pli: ProductFeatureKeyType;
@ -73,7 +75,7 @@ export const registerUpsellings = (
const upsellingSectionsToRegister = upsellingSections.reduce<SectionUpsellings>(
(sectionUpsellings, { id, pli, component }) => {
if (!enabledPLIsSet.has(pli)) {
sectionUpsellings[id] = component;
sectionUpsellings[id] = withServicesProvider(component, services);
}
return sectionUpsellings;
},
@ -102,8 +104,9 @@ export const upsellingPages: UpsellingPages = [
pageName: SecurityPageName.entityAnalytics,
pli: ProductFeatureKey.advancedInsights,
component: () => (
<EntityAnalyticsUpsellingLazy
requiredProduct={getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? undefined}
<EntityAnalyticsUpsellingPageLazy
upgradeToLabel={entityAnalyticsProductType}
upgradeMessage={i18n.UPGRADE_PRODUCT_MESSAGE(entityAnalyticsProductType)}
/>
),
},
@ -123,6 +126,8 @@ export const upsellingPages: UpsellingPages = [
},
];
const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? '';
// Upselling 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.
@ -155,6 +160,16 @@ export const upsellingSections: UpsellingSections = [
pli: ProductFeatureKey.endpointProtectionUpdates,
component: EndpointProtectionUpdatesLazy,
},
{
id: 'entity_analytics_panel',
pli: ProductFeatureKey.advancedInsights,
component: () => (
<EntityAnalyticsUpsellingSectionLazy
upgradeToLabel={entityAnalyticsProductType}
upgradeMessage={i18n.UPGRADE_PRODUCT_MESSAGE(entityAnalyticsProductType)}
/>
),
},
];
// Upselling for sections, linked by arbitrary ids

View file

@ -0,0 +1,20 @@
/*
* 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_PRODUCT_MESSAGE = (requiredProduct: string) =>
i18n.translate(
'xpack.securitySolutionServerless.upselling.entityAnalytics.upgradeProductMessage',
{
defaultMessage:
'Entity risk scoring capability is available in our {requiredProduct} license tier',
values: {
requiredProduct,
},
}
);

View file

@ -5879,8 +5879,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.unmanagedPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {n'est pas géré} other {ne sont pas gérés}} par la gestion du cycle de vie des index (ILM)",
"securitySolutionPackages.ecsDataQualityDashboard.warmPatternTooltip": "{indices} {indices, plural, =1 {L'index correspondant} other {Les index correspondants}} au modèle {pattern} {indices, plural, =1 {est} other {sont}} \"warm\". Les index \"warm\" ne sont plus mis à jour, mais ils sont toujours interrogés.",
"securitySolutionPackages.entityAnalytics.paywall.upgradeButton": "Passer à {requiredLicenseOrProduct}",
"securitySolutionPackages.entityAnalytics.paywall.upgradeLicenseMessage": "Cette fonctionnalité est disponible avec l'abonnement {requiredLicense} ou supérieur",
"securitySolutionPackages.entityAnalytics.paywall.upgradeProductMessage": "La capacité d'évaluation du risque des entités est disponible dans notre niveau de licence {requiredProduct}",
"securitySolutionPackages.markdown.insight.upsell": "Passez au niveau {requiredLicense} pour pouvoir utiliser les informations des guides d'investigation",
"securitySolutionPackages.alertSuppressionRuleDetails.upsell": "La suppression d'alertes est configurée mais elle ne sera pas appliquée en raison d'une licence insuffisante",
"securitySolutionPackages.beta.label": "Bêta",

View file

@ -5870,8 +5870,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.unmanagedPatternTooltip": "{pattern}パターンと一致する{indices} {indices, plural, other {インデックス}}{indices, plural, other {は}}インデックスライフサイクル管理ILMによって管理されていません",
"securitySolutionPackages.ecsDataQualityDashboard.warmPatternTooltip": "{pattern}パターンと一致する{indices} {indices, plural, other {インデックス}}{indices, plural, other {は}}ウォームです。ウォームインデックスは更新されませんが、まだ照会されています。",
"securitySolutionPackages.entityAnalytics.paywall.upgradeButton": "{requiredLicenseOrProduct}にアップグレード",
"securitySolutionPackages.entityAnalytics.paywall.upgradeLicenseMessage": "この機能は、{requiredLicense}以上のサブスクリプションでご利用いただけます",
"securitySolutionPackages.entityAnalytics.paywall.upgradeProductMessage": "エンティティリスクスコアリング機能は、{requiredProduct}ライセンスティアで利用可能です",
"securitySolutionPackages.alertSuppressionRuleDetails.upsell": "アラート非表示が構成されていますが、ライセンス不足のため適用されません",
"securitySolutionPackages.beta.label": "ベータ",
"securitySolutionPackages.dataTable.ariaLabel": "アラート",

View file

@ -5883,7 +5883,6 @@
"securitySolutionPackages.ecsDataQualityDashboard.unmanagedPatternTooltip": "与 {pattern} 模式匹配的 {indices} 个{indices, plural, other {索引}}{indices, plural, other {}}不通过索引生命周期管理 (ILM) 进行管理",
"securitySolutionPackages.ecsDataQualityDashboard.warmPatternTooltip": "{indices} 个匹配 {pattern} 模式的{indices, plural, other {索引}}{indices, plural, other {为}}温索引。不再更新但仍会查询温索引。",
"securitySolutionPackages.entityAnalytics.paywall.upgradeButton": "升级到 {requiredLicenseOrProduct}",
"securitySolutionPackages.entityAnalytics.paywall.upgradeLicenseMessage": "{requiredLicense}或更高级订阅可以使用此功能",
"securitySolutionPackages.markdown.insight.upsell": "升级到{requiredLicense}以利用调查指南中的洞见",
"securitySolutionPackages.alertSuppressionRuleDetails.upsell": "已配置告警阻止,但由于许可不足而无法应用",
"securitySolutionPackages.beta.label": "公测版",