[Security Solutions] Add PLI authorisation for Advanced Insights (Entity Risk) (#161190)

## Summary

Add PLI authorization checks for Entity Analytics features.
*This PR only restricts access to the features* but doesn't implement
PLG/Upselling. It will be added later when we have defined the UX for
it.

The `advancedInsights` PLI was already configured, so I only had to add
extra checks to make sure users can't see the Risk score on other
components.
Updated components:
* "All hosts" table on the Hosts page
* "All users" table on the Users page
* Host overview on the Host details page and Host details flyout
* User overview on the User details page and User details flyout
* Alerts flyout
* Remove sample Upselling components config

### Not included
* Upselling/PLG
* I left empty tabs/pages where the Upselling component will be added

### How to test it?
#### ESS
* Run ESS with a basic license
* Run ESS with a platinum

#### Serverless
* Run Serverless with security essentials (serverless.security.yml)
```
xpack.serverless.security.productTypes:
  [
    { product_line: 'security', product_tier: 'essentials' }
  ]
```
* Run Serverless with security complete
(kibana/config/serverless.security.yml)
```
xpack.serverless.security.productTypes:
  [
    { product_line: 'security', product_tier: 'complete' },
  ]
 
 ```


1ab84134-bee1-497c-9b41-a9ec398bd921

### 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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2023-07-26 13:12:09 +02:00 committed by GitHub
parent c0cb6133ea
commit a074c06864
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 272 additions and 154 deletions

View file

@ -24,6 +24,10 @@ jest.mock('../../../lib/kibana', () => ({
}),
}));
jest.mock('../../../../helper_hooks', () => ({
useHasSecurityCapability: () => true,
}));
jest.mock('../table/field_name_cell');
const RISK_SCORE_DATA_ROWS = 2;

View file

@ -30,6 +30,7 @@ import { RiskSummary } from './risk_summary';
import { EnrichmentSummary } from './enrichment_summary';
import type { HostRisk, UserRisk } from '../../../../explore/containers/risk_score';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { useHasSecurityCapability } from '../../../../helper_hooks';
const UppercaseEuiTitle = styled(EuiTitle)`
text-transform: uppercase;
@ -151,6 +152,12 @@ const ThreatSummaryViewComponent: React.FC<{
(eventDetail) => eventDetail?.field === 'user.risk.calculated_level'
)?.values?.[0] as RiskSeverity | undefined;
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
if (!hasEntityAnalyticsCapability && enrichments.length === 0) {
return null;
}
return (
<>
<EuiHorizontalRule />
@ -161,21 +168,25 @@ const ThreatSummaryViewComponent: React.FC<{
<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="m" style={{ flexGrow: 0 }}>
<EuiFlexItem grow={false}>
<RiskSummary
riskEntity={RiskScoreEntity.host}
risk={hostRisk}
originalRisk={originalHostRisk}
/>
</EuiFlexItem>
{hasEntityAnalyticsCapability && (
<>
<EuiFlexItem grow={false}>
<RiskSummary
riskEntity={RiskScoreEntity.host}
risk={hostRisk}
originalRisk={originalHostRisk}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RiskSummary
riskEntity={RiskScoreEntity.user}
risk={userRisk}
originalRisk={originalUserRisk}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RiskSummary
riskEntity={RiskScoreEntity.user}
risk={userRisk}
originalRisk={originalUserRisk}
/>
</EuiFlexItem>
</>
)}
<EnrichmentSummary
browserFields={browserFields}

View file

@ -198,7 +198,7 @@ const EventDetailsComponent: React.FC<Props> = ({
const enrichmentCount = allEnrichments.length;
const { hostRisk, userRisk, isLicenseValid } = useRiskScoreData(data);
const { hostRisk, userRisk, isAuthorized } = useRiskScoreData(data);
const renderer = useMemo(
() =>
@ -212,9 +212,9 @@ const EventDetailsComponent: React.FC<Props> = ({
const showThreatSummary = useMemo(() => {
const hasEnrichments = enrichmentCount > 0;
const hasRiskInfoWithLicense = isLicenseValid && (hostRisk || userRisk);
const hasRiskInfoWithLicense = isAuthorized && (hostRisk || userRisk);
return hasEnrichments || hasRiskInfoWithLicense;
}, [enrichmentCount, hostRisk, isLicenseValid, userRisk]);
}, [enrichmentCount, hostRisk, isAuthorized, userRisk]);
const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled(
'endpointResponseActionsEnabled'
);

View file

@ -20,7 +20,7 @@ const defaultResult = {
data: [],
inspect: {},
isInspected: false,
isLicenseValid: true,
isAuthorized: true,
isModuleEnabled: true,
refetch: () => {},
totalCount: 0,
@ -55,7 +55,7 @@ describe('useRiskScoreData', () => {
expect(result.current).toEqual({
hostRisk: defaultRisk,
userRisk: defaultRisk,
isLicenseValid: true,
isAuthorized: true,
});
});

View file

@ -31,7 +31,7 @@ export const useRiskScoreData = (data: TimelineEventsDetailsItem[]) => {
const {
data: hostRiskData,
loading: hostRiskLoading,
isLicenseValid: isHostLicenseValid,
isAuthorized: isHostRiskScoreAuthorized,
isModuleEnabled: isHostRiskModuleEnabled,
} = useRiskScore({
filterQuery: hostNameFilterQuery,
@ -57,7 +57,7 @@ export const useRiskScoreData = (data: TimelineEventsDetailsItem[]) => {
const {
data: userRiskData,
loading: userRiskLoading,
isLicenseValid: isUserLicenseValid,
isAuthorized: isUserRiskScoreAuthorized,
isModuleEnabled: isUserRiskModuleEnabled,
} = useRiskScore({
filterQuery: userNameFilterQuery,
@ -75,5 +75,9 @@ export const useRiskScoreData = (data: TimelineEventsDetailsItem[]) => {
[userRiskLoading, isUserRiskModuleEnabled, userRiskData]
);
return { userRisk, hostRisk, isLicenseValid: isHostLicenseValid && isUserLicenseValid };
return {
userRisk,
hostRisk,
isAuthorized: isHostRiskScoreAuthorized && isUserRiskScoreAuthorized,
};
};

View file

@ -34,6 +34,7 @@ import {
DEFAULT_RULES_TABLE_REFRESH_SETTING,
DEFAULT_RULE_REFRESH_INTERVAL_ON,
DEFAULT_RULE_REFRESH_INTERVAL_VALUE,
SERVER_APP_ID,
} from '../../../../common/constants';
import type { StartServices } from '../../../types';
import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage';
@ -178,6 +179,16 @@ export const createStartServicesMock = (
})),
},
},
application: {
...core.application,
capabilities: {
...core.application.capabilities,
[SERVER_APP_ID]: {
crud: true,
read: true,
},
},
},
security,
storage,
fleet,

View file

@ -60,7 +60,7 @@ describe('AlertDetailsPage - SummaryTab - HostPanel', () => {
inspect: null,
refetch: () => {},
isModuleEnabled: true,
isLicenseValid: true,
isAuthorized: true,
loading: false,
};
const HostPanelWithDefaultProps = (propOverrides: Partial<HostPanelProps>) => (
@ -106,7 +106,7 @@ describe('AlertDetailsPage - SummaryTab - HostPanel', () => {
it('should not show risk if the license is not valid', () => {
mockUseRiskScore.mockReturnValue({
...defaultRiskReturnValues,
isLicenseValid: false,
isAuthorized: false,
data: null,
});
const { queryByTestId } = render(<HostPanelWithDefaultProps />);
@ -119,7 +119,7 @@ describe('AlertDetailsPage - SummaryTab - HostPanel', () => {
mockUseRiskScore.mockReturnValue({
...defaultRiskReturnValues,
isLicenseValid: true,
isAuthorized: true,
data: [
{
host: {

View file

@ -88,7 +88,7 @@ export const HostPanel = React.memo(
);
}, [browserFields, data, id]);
const { data: hostRisk, isLicenseValid: isRiskLicenseValid } = useRiskScore({
const { data: hostRisk, isAuthorized: isRiskScoreAuthorized } = useRiskScore({
riskEntity: RiskScoreEntity.host,
skip: hostName == null,
});
@ -149,7 +149,7 @@ export const HostPanel = React.memo(
)}
</EuiFlexGroup>
<EuiSpacer size="l" />
{isRiskLicenseValid && (
{isRiskScoreAuthorized && (
<>
<EuiFlexGroup data-test-subj="host-panel-risk">
{hostRiskScore && (

View file

@ -62,7 +62,7 @@ export const UserPanel = React.memo(
({ data, selectedPatterns, openUserDetailsPanel }: UserPanelProps) => {
const userName = useMemo(() => getTimelineEventData('user.name', data), [data]);
const { data: userRisk, isLicenseValid: isRiskLicenseValid } = useRiskScore({
const { data: userRisk, isAuthorized: isRiskScoreAuthorized } = useRiskScore({
riskEntity: RiskScoreEntity.user,
skip: userName == null,
});
@ -114,7 +114,7 @@ export const UserPanel = React.memo(
</UserPanelSection>
</EuiFlexGroup>
<EuiSpacer size="l" />
{isRiskLicenseValid && (
{isRiskScoreAuthorized && (
<>
<EuiFlexGroup data-test-subj="user-panel-risk">
{userRiskScore && (

View file

@ -27,7 +27,7 @@ describe('AlertDetailsPage - SummaryTab - UserPanel', () => {
inspect: null,
refetch: () => {},
isModuleEnabled: true,
isLicenseValid: true,
isAuthorized: true,
loading: false,
};
const UserPanelWithDefaultProps = (propOverrides: Partial<UserPanelProps>) => (
@ -64,7 +64,7 @@ describe('AlertDetailsPage - SummaryTab - UserPanel', () => {
it('should not show risk if the license is not valid', () => {
mockUseRiskScore.mockReturnValue({
...defaultRiskReturnValues,
isLicenseValid: false,
isAuthorized: false,
data: null,
});
const { queryByTestId } = render(<UserPanelWithDefaultProps />);
@ -77,7 +77,7 @@ describe('AlertDetailsPage - SummaryTab - UserPanel', () => {
mockUseRiskScore.mockReturnValue({
...defaultRiskReturnValues,
isLicenseValid: true,
isAuthorized: true,
data: [
{
user: {

View file

@ -32,7 +32,6 @@ import type { UsersComponentsQueryProps } from '../../../users/pages/navigation/
import type { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types';
import { useDashboardHref } from '../../../../common/hooks/use_dashboard_href';
import { RiskScoresNoDataDetected } from '../risk_score_onboarding/risk_score_no_data_detected';
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
margin-top: ${({ theme }) => theme.eui.euiSizeL};
@ -49,7 +48,6 @@ const RiskDetailsTabBodyComponent: React.FC<
riskEntity: RiskScoreEntity;
}
> = ({ entityName, startDate, endDate, setQuery, deleteQuery, riskEntity }) => {
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
const queryId = useMemo(
() =>
riskEntity === RiskScoreEntity.host
@ -84,13 +82,14 @@ const RiskDetailsTabBodyComponent: React.FC<
[entityName, riskEntity]
);
const { data, loading, refetch, inspect, isDeprecated, isModuleEnabled } = useRiskScore({
filterQuery,
onlyLatest: false,
riskEntity,
skip: !overTimeToggleStatus && !contributorsToggleStatus,
timerange,
});
const { data, loading, refetch, inspect, isDeprecated, isModuleEnabled, isAuthorized } =
useRiskScore({
filterQuery,
onlyLatest: false,
riskEntity,
skip: !overTimeToggleStatus && !contributorsToggleStatus,
timerange,
});
const rules = useMemo(() => {
const lastRiskItem = data && data.length > 0 ? data[data.length - 1] : null;
@ -130,8 +129,8 @@ const RiskDetailsTabBodyComponent: React.FC<
isDeprecated: isDeprecated && !loading,
};
if (RiskScoreUpsell) {
return <RiskScoreUpsell />;
if (!isAuthorized) {
return <>{'TODO: Add RiskScore Upsell'}</>;
}
if (status.isDisabled || status.isDeprecated) {

View file

@ -34,7 +34,7 @@ let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
const defaultFeatureStatus = {
isLoading: false,
isDeprecated: false,
isLicenseValid: true,
isAuthorized: true,
isEnabled: true,
refetch: () => {},
};
@ -42,7 +42,7 @@ const defaultRisk = {
data: undefined,
inspect: {},
isInspected: false,
isLicenseValid: true,
isAuthorized: true,
isModuleEnabled: true,
isDeprecated: false,
totalCount: 0,
@ -72,7 +72,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
test('does not search if license is not valid', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isLicenseValid: false,
isAuthorized: false,
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
@ -81,7 +81,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isLicenseValid: false,
isAuthorized: false,
refetch: result.current.refetch,
});
});

View file

@ -40,7 +40,7 @@ export interface RiskScoreState<T extends RiskScoreEntity.host | RiskScoreEntity
refetch: inputsModel.Refetch;
totalCount: number;
isModuleEnabled: boolean;
isLicenseValid: boolean;
isAuthorized: boolean;
isDeprecated: boolean;
loading: boolean;
}
@ -98,7 +98,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
const {
isDeprecated,
isEnabled,
isLicenseValid,
isAuthorized,
isLoading: isDeprecatedLoading,
refetch: refetchDeprecated,
} = useRiskScoreFeatureStatus(riskEntity, defaultIndex);
@ -136,20 +136,12 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
inspect,
refetch: refetchAll,
totalCount: response.totalCount,
isLicenseValid,
isAuthorized,
isDeprecated,
isModuleEnabled: isEnabled,
isInspected: false,
}),
[
inspect,
isDeprecated,
isEnabled,
isLicenseValid,
refetchAll,
response.data,
response.totalCount,
]
[inspect, isDeprecated, isEnabled, isAuthorized, refetchAll, response.data, response.totalCount]
);
const requestTimerange = useMemo(
@ -205,21 +197,13 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
!skip &&
!isDeprecatedLoading &&
riskScoreRequest != null &&
isLicenseValid &&
isAuthorized &&
isEnabled &&
!isDeprecated
) {
search(riskScoreRequest);
}
}, [
isEnabled,
isDeprecated,
isLicenseValid,
isDeprecatedLoading,
riskScoreRequest,
search,
skip,
]);
}, [isEnabled, isDeprecated, isAuthorized, isDeprecatedLoading, riskScoreRequest, search, skip]);
return { ...riskScoreResponse, loading: loading || isDeprecatedLoading };
};

View file

@ -11,19 +11,23 @@ import { useRiskScoreFeatureStatus } from '.';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { useFetch } from '../../../../common/hooks/use_fetch';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
import { useHasSecurityCapability } from '../../../../helper_hooks';
jest.mock('../../../../common/hooks/use_fetch');
jest.mock('../../../../common/components/ml/hooks/use_ml_capabilities');
jest.mock('../../../../helper_hooks');
const mockFetch = jest.fn();
const mockUseMlCapabilities = useMlCapabilities as jest.Mock;
const mockUseFetch = useFetch as jest.Mock;
const mockUseHasSecurityCapability = useHasSecurityCapability as jest.Mock;
describe(`risk score feature status`, () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: true });
mockUseFetch.mockReturnValue(defaultFetch);
mockUseHasSecurityCapability.mockReturnValue(true);
});
const defaultFetch = {
@ -36,7 +40,7 @@ describe(`risk score feature status`, () => {
const defaultResult = {
error: undefined,
isDeprecated: true,
isLicenseValid: true,
isAuthorized: true,
isEnabled: true,
isLoading: true,
};
@ -52,7 +56,25 @@ describe(`risk score feature status`, () => {
expect(mockFetch).not.toHaveBeenCalled();
expect(result.current).toEqual({
...defaultResult,
isLicenseValid: false,
isAuthorized: false,
isDeprecated: false,
isEnabled: false,
refetch: result.current.refetch,
});
});
test("does not search if the user doesn't has entity analytics capability", () => {
mockUseHasSecurityCapability.mockReturnValue(false);
const { result } = renderHook(
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
{
wrapper: TestProviders,
}
);
expect(mockFetch).not.toHaveBeenCalled();
expect(result.current).toEqual({
...defaultResult,
isAuthorized: false,
isDeprecated: false,
isEnabled: false,
refetch: result.current.refetch,

View file

@ -10,15 +10,16 @@ import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml
import { REQUEST_NAMES, useFetch } from '../../../../common/hooks/use_fetch';
import type { RiskScoreEntity } from '../../../../../common/search_strategy';
import { getRiskScoreIndexStatus } from './api';
import { useHasSecurityCapability } from '../../../../helper_hooks';
interface RiskScoresFeatureStatus {
error: unknown;
// Is transform index an old version?
isDeprecated: boolean;
// does the transform index exist?
// Does the transform index exist?
isEnabled: boolean;
// is the user's license platinum?
isLicenseValid: boolean;
// Does the user has the authorization for the risk score feature?
isAuthorized: boolean;
isLoading: boolean;
refetch: (indexName: string) => void;
}
@ -28,6 +29,8 @@ export const useRiskScoreFeatureStatus = (
defaultIndex?: string
): RiskScoresFeatureStatus => {
const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities();
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const isAuthorized = isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
const { fetch, data, isLoading, error } = useFetch(
REQUEST_NAMES.GET_RISK_SCORE_DEPRECATED,
@ -35,35 +38,36 @@ export const useRiskScoreFeatureStatus = (
);
const response = useMemo(
// if license is enabled, let isDeprecated = true so the actual
// if authorized is true, let isDeprecated = true so the actual
// risk score fetch is not called until this check is complete
() =>
data ? data : { isDeprecated: isPlatinumOrTrialLicense, isEnabled: isPlatinumOrTrialLicense },
// isPlatinumOrTrialLicense is initial state, not update requirement
() => (data ? data : { isDeprecated: isAuthorized, isEnabled: isAuthorized }),
// isAuthorized is initial state, not update requirement
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
);
const searchIndexStatus = useCallback(
(indexName: string) => {
fetch({
query: { indexName, entity: riskEntity },
});
if (isAuthorized) {
fetch({
query: { indexName, entity: riskEntity },
});
}
},
[riskEntity, fetch]
[isAuthorized, fetch, riskEntity]
);
useEffect(() => {
if (isPlatinumOrTrialLicense && defaultIndex != null) {
if (defaultIndex != null) {
searchIndexStatus(defaultIndex);
}
}, [isPlatinumOrTrialLicense, defaultIndex, searchIndexStatus]);
}, [defaultIndex, searchIndexStatus]);
return {
error,
isLoading: isLoading || !capabilitiesFetched || defaultIndex == null,
refetch: searchIndexStatus,
isLicenseValid: isPlatinumOrTrialLicense,
isAuthorized,
...response,
};
};

View file

@ -50,6 +50,11 @@ jest.mock('../../../../common/components/ml/hooks/use_ml_capabilities', () => ({
useMlCapabilities: () => mockUseMlCapabilities(),
}));
const mockUseHasSecurityCapability = jest.fn().mockReturnValue(false);
jest.mock('../../../../helper_hooks', () => ({
useHasSecurityCapability: () => mockUseHasSecurityCapability(),
}));
describe('Hosts Table', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
@ -84,8 +89,9 @@ describe('Hosts Table', () => {
expect(wrapper.find('HostsTable')).toMatchSnapshot();
});
test('it renders "Host Risk classfication" column when "isPlatinumOrTrialLicense" is truthy', () => {
test('it renders "Host Risk classification" column when "isPlatinumOrTrialLicense" is truthy and user has risk-entity capability', () => {
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: true });
mockUseHasSecurityCapability.mockReturnValue(true);
const { queryByTestId } = render(
<TestProviders store={store}>
@ -107,8 +113,33 @@ describe('Hosts Table', () => {
expect(queryByTestId('tableHeaderCell_node.risk_4')).toBeInTheDocument();
});
test("it doesn't renders 'Host Risk classfication' column when 'isPlatinumOrTrialLicense' is falsy", () => {
test("it doesn't renders 'Host Risk classification' column when 'isPlatinumOrTrialLicense' is falsy", () => {
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: false });
mockUseHasSecurityCapability.mockReturnValue(true);
const { queryByTestId } = render(
<TestProviders store={store}>
<HostsTable
id="hostsQuery"
isInspect={false}
loading={false}
data={mockData}
totalCount={0}
fakeTotalCount={-1}
setQuerySkip={jest.fn()}
showMorePagesIndicator={false}
loadPage={loadPage}
type={hostsModel.HostsType.page}
/>
</TestProviders>
);
expect(queryByTestId('tableHeaderCell_node.riskScore_4')).not.toBeInTheDocument();
});
test("it doesn't renders 'Host Risk classification' column when user doesn't has entity-analytics capabilities", () => {
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: true });
mockUseHasSecurityCapability.mockReturnValue(false);
const { queryByTestId } = render(
<TestProviders store={store}>

View file

@ -31,6 +31,7 @@ import { SecurityPageName } from '../../../../../common/constants';
import { HostsTableType } from '../../store/model';
import { useNavigateTo } from '../../../../common/lib/kibana/hooks';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
import { useHasSecurityCapability } from '../../../../helper_hooks';
const tableType = hostsModel.HostsTableType.hosts;
@ -132,6 +133,8 @@ const HostsTableComponent: React.FC<HostsTableProps> = ({
},
[direction, sortField, type, dispatch]
);
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const dispatchSeverityUpdate = useCallback(
@ -151,8 +154,12 @@ const HostsTableComponent: React.FC<HostsTableProps> = ({
);
const hostsColumns = useMemo(
() => getHostsColumns(isPlatinumOrTrialLicense, dispatchSeverityUpdate),
[dispatchSeverityUpdate, isPlatinumOrTrialLicense]
() =>
getHostsColumns(
isPlatinumOrTrialLicense && hasEntityAnalyticsCapability,
dispatchSeverityUpdate
),
[dispatchSeverityUpdate, isPlatinumOrTrialLicense, hasEntityAnalyticsCapability]
);
const sorting = useMemo(() => getSorting(sortField, direction), [sortField, direction]);

View file

@ -22,7 +22,6 @@ import {
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
import { RiskScoresNoDataDetected } from '../../../components/risk_score/risk_score_onboarding/risk_score_no_data_detected';
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
const HostRiskScoreTableManage = manageQuery(HostRiskScoreTable);
@ -35,7 +34,6 @@ export const HostRiskScoreQueryTabBody = ({
startDate: from,
type,
}: HostsComponentsQueryProps) => {
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
const getHostRiskScoreSelector = useMemo(() => hostsSelectors.hostRiskScoreSelector(), []);
const { activePage, limit, sort } = useDeepEqualSelector((state: State) =>
getHostRiskScoreSelector(state, hostsModel.HostsType.page)
@ -71,6 +69,7 @@ export const HostRiskScoreQueryTabBody = ({
isModuleEnabled,
loading,
refetch,
isAuthorized,
totalCount,
} = useRiskScore({
filterQuery,
@ -92,8 +91,8 @@ export const HostRiskScoreQueryTabBody = ({
isDeprecated: isDeprecated && !loading,
};
if (RiskScoreUpsell) {
return <RiskScoreUpsell />;
if (!isAuthorized) {
return <>{'TODO: Add RiskScore Upsell'}</>;
}
if (status.isDisabled || status.isDeprecated) {

View file

@ -24,7 +24,6 @@ import {
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
import { RiskScoresNoDataDetected } from '../../../components/risk_score/risk_score_onboarding/risk_score_no_data_detected';
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable);
@ -37,8 +36,6 @@ export const UserRiskScoreQueryTabBody = ({
startDate: from,
type,
}: UsersComponentsQueryProps) => {
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
const getUserRiskScoreSelector = useMemo(() => usersSelectors.userRiskScoreSelector(), []);
const { activePage, limit, sort } = useDeepEqualSelector((state: State) =>
getUserRiskScoreSelector(state)
@ -75,6 +72,7 @@ export const UserRiskScoreQueryTabBody = ({
loading,
refetch,
totalCount,
isAuthorized,
} = useRiskScore({
filterQuery,
pagination,
@ -95,8 +93,8 @@ export const UserRiskScoreQueryTabBody = ({
isDeprecated: isDeprecated && !loading,
};
if (RiskScoreUpsell) {
return <RiskScoreUpsell />;
if (!isAuthorized) {
return <>{'TODO: Add RiskScore Upsell'}</>;
}
if (status.isDisabled || status.isDeprecated) {

View file

@ -55,6 +55,11 @@ jest.mock('uuid', () => ({
jest.mock('../../../common/components/ml/hooks/use_ml_capabilities');
const mockUseMlUserPermissions = useMlCapabilities as jest.Mock;
const mockUseHasSecurityCapability = jest.fn().mockReturnValue(false);
jest.mock('../../../helper_hooks', () => ({
useHasSecurityCapability: () => mockUseHasSecurityCapability(),
}));
jest.mock('../../../common/containers/sourcerer', () => ({
useSourcererDataView: jest.fn().mockReturnValue({ selectedPatterns: ['index'] }),
}));
@ -105,7 +110,7 @@ const mockRiskScoreResponse = {
},
},
],
isLicenseValid: true,
isAuthorized: true,
};
const mockRelatedUsersResponse = {
@ -150,11 +155,13 @@ describe('<HostDetails />', () => {
expect(getByTestId(HOST_DETAILS_INFO_TEST_ID)).toBeInTheDocument();
});
it('should render host risk score when license is valid', () => {
it('should render host risk score when authorized', () => {
mockUseMlUserPermissions.mockReturnValue({
isPlatinumOrTrialLicense: true,
capabilities: {},
});
mockUseRiskScore.mockReturnValue({ data: [], isAuthorized: true });
const { getByText } = render(
<TestProviders>
<HostDetails {...defaultProps} />
@ -163,8 +170,8 @@ describe('<HostDetails />', () => {
expect(getByText('Host risk score')).toBeInTheDocument();
});
it('should not render host risk score when license is not valid', () => {
mockUseRiskScore.mockReturnValue({ data: [], isLicenseValid: false });
it('should not render host risk score when unauthorized', () => {
mockUseRiskScore.mockReturnValue({ data: [], isAuthorized: false });
const { queryByText } = render(
<TestProviders>
<HostDetails {...defaultProps} />
@ -190,11 +197,13 @@ describe('<HostDetails />', () => {
expect(getByTestId(HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID)).toBeInTheDocument();
});
it('should render user risk score column when license is valid', () => {
it('should render user risk score column when license and capabilities are valid', () => {
mockUseMlUserPermissions.mockReturnValue({
isPlatinumOrTrialLicense: true,
capabilities: {},
});
mockUseHasSecurityCapability.mockReturnValue(true);
const { queryAllByRole } = render(
<TestProviders>
<HostDetails {...defaultProps} />
@ -206,6 +215,21 @@ describe('<HostDetails />', () => {
expect(queryAllByRole('row')[1].textContent).toContain('Low');
});
it('should not render host risk score column when user has no entity-risk capability', () => {
mockUseMlUserPermissions.mockReturnValue({
isPlatinumOrTrialLicense: true,
capabilities: {},
});
mockUseHasSecurityCapability.mockReturnValue(false);
const { queryAllByRole } = render(
<TestProviders>
<HostDetails {...defaultProps} />
</TestProviders>
);
expect(queryAllByRole('columnheader').length).toBe(2);
});
it('should not render host risk score column when license is not valid', () => {
const { queryAllByRole } = render(
<TestProviders>

View file

@ -50,6 +50,7 @@ import { HOST_DETAILS_TEST_ID, HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID } from '
import { ENTITY_RISK_CLASSIFICATION } from '../../../explore/components/risk_score/translations';
import { USER_RISK_TOOLTIP } from '../../../explore/users/components/all_users/translations';
import * as i18n from './translations';
import { useHasSecurityCapability } from '../../../helper_hooks';
const HOST_DETAILS_ID = 'entities-hosts-details';
const RELATED_USERS_ID = 'entities-hosts-related-users';
@ -77,7 +78,10 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp })
// create a unique, but stable (across re-renders) query id
const hostDetailsQueryId = useMemo(() => `${HOST_DETAILS_ID}-${uuid()}`, []);
const relatedUsersQueryId = useMemo(() => `${RELATED_USERS_ID}-${uuid()}`, []);
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const isEntityAnalyticsAuthorized = isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
const narrowDateRange = useCallback(
(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
@ -151,7 +155,7 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp })
);
},
},
...(isPlatinumOrTrialLicense
...(isEntityAnalyticsAuthorized
? [
{
field: 'risk',
@ -176,7 +180,7 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp })
]
: []),
],
[isPlatinumOrTrialLicense]
[isEntityAnalyticsAuthorized]
);
const relatedUsersCount = useMemo(

View file

@ -71,6 +71,8 @@ jest.mock('../../../common/components/ml/anomaly/anomaly_table_provider', () =>
}) => children({ anomaliesData: mockAnomalies, isLoadingAnomaliesData: false, jobNameById: {} }),
}));
jest.mock('../../../helper_hooks', () => ({ useHasSecurityCapability: () => true }));
jest.mock('../../../explore/users/containers/users/observed_details');
const mockUseObservedUserDetails = useObservedUserDetails as jest.Mock;
@ -105,7 +107,7 @@ const mockRiskScoreResponse = {
},
},
],
isLicenseValid: true,
isAuthorized: true,
};
const mockRelatedHostsResponse = {
@ -165,7 +167,7 @@ describe('<HostDetails />', () => {
});
it('should not render user risk score when license is not valid', () => {
mockUseRiskScore.mockReturnValue({ data: [], isLicenseValid: false });
mockUseRiskScore.mockReturnValue({ data: [], isAuthorized: false });
const { queryByText } = render(
<TestProviders>
<UserDetails {...defaultProps} />

View file

@ -50,6 +50,7 @@ import { USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID, USER_DETAILS_TEST_ID } from '
import { ENTITY_RISK_CLASSIFICATION } from '../../../explore/components/risk_score/translations';
import { HOST_RISK_TOOLTIP } from '../../../explore/hosts/components/hosts_table/translations';
import * as i18n from './translations';
import { useHasSecurityCapability } from '../../../helper_hooks';
const USER_DETAILS_ID = 'entities-users-details';
const RELATED_HOSTS_ID = 'entities-users-related-hosts';
@ -77,7 +78,11 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp })
// create a unique, but stable (across re-renders) query id
const userDetailsQueryId = useMemo(() => `${USER_DETAILS_ID}-${uuid()}`, []);
const relatedHostsQueryId = useMemo(() => `${RELATED_HOSTS_ID}-${uuid()}`, []);
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const isEntityAnalyticsAuthorized = isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
const narrowDateRange = useCallback(
(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
@ -151,7 +156,7 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp })
);
},
},
...(isPlatinumOrTrialLicense
...(isEntityAnalyticsAuthorized
? [
{
field: 'risk',
@ -176,7 +181,7 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp })
]
: []),
],
[isPlatinumOrTrialLicense]
[isEntityAnalyticsAuthorized]
);
const relatedHostsCount = useMemo(

View file

@ -48,7 +48,7 @@ describe('<HostEntityContent />', () => {
describe('license is valid', () => {
it('should render ip addresses and host risk classification', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
mockUseRiskScore.mockReturnValue({ data: riskLevel, isLicenseValid: true });
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
const { getByTestId } = render(
<TestProviders>
@ -63,7 +63,7 @@ describe('<HostEntityContent />', () => {
it('should render correctly if returned data is null', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isLicenseValid: true });
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: true });
const { getByTestId } = render(
<TestProviders>
@ -79,7 +79,7 @@ describe('<HostEntityContent />', () => {
describe('license is not valid', () => {
it('should render ip but not host risk classification', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
mockUseRiskScore.mockReturnValue({ data: riskLevel, isLicenseValid: false });
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<HostEntityOverview hostName={hostName} />
@ -92,7 +92,7 @@ describe('<HostEntityContent />', () => {
it('should render correctly if returned data is null', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isLicenseValid: false });
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<HostEntityOverview hostName={hostName} />

View file

@ -66,7 +66,7 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
[hostName]
);
const { data: hostRisk, isLicenseValid } = useRiskScore({
const { data: hostRisk, isAuthorized } = useRiskScore({
filterQuery,
riskEntity: RiskScoreEntity.host,
skip: hostName == null,
@ -138,7 +138,7 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
/>
</EuiFlexItem>
<EuiFlexItem>
{isLicenseValid && (
{isAuthorized && (
<DescriptionListStyled
data-test-subj={ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID}
listItems={[hostRiskLevel]}

View file

@ -49,7 +49,7 @@ describe('<UserEntityOverview />', () => {
describe('license is valid', () => {
it('should render ip addresses and user risk classification', () => {
mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]);
mockUseRiskScore.mockReturnValue({ data: riskLevel, isLicenseValid: true });
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
const { getByTestId } = render(
<TestProviders>
@ -64,7 +64,7 @@ describe('<UserEntityOverview />', () => {
it('should render correctly if returned data is null', () => {
mockUseUserDetails.mockReturnValue([false, { userDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isLicenseValid: true });
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: true });
const { getByTestId } = render(
<TestProviders>
@ -80,7 +80,7 @@ describe('<UserEntityOverview />', () => {
describe('license is not valid', () => {
it('should render ip but not user risk classification', () => {
mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]);
mockUseRiskScore.mockReturnValue({ data: riskLevel, isLicenseValid: false });
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<UserEntityOverview userName={userName} />
@ -93,7 +93,7 @@ describe('<UserEntityOverview />', () => {
it('should render correctly if returned data is null', () => {
mockUseUserDetails.mockReturnValue([false, { userDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isLicenseValid: false });
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<UserEntityOverview userName={userName} />

View file

@ -73,7 +73,7 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
startDate: from,
});
const { data: userRisk, isLicenseValid } = useRiskScore({
const { data: userRisk, isAuthorized } = useRiskScore({
filterQuery,
riskEntity: RiskScoreEntity.user,
timerange,
@ -138,7 +138,7 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
/>
</EuiFlexItem>
<EuiFlexItem>
{isLicenseValid && (
{isAuthorized && (
<DescriptionListStyled
data-test-subj={ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID}
listItems={[userRiskLevel]}

View file

@ -6,6 +6,7 @@
*/
import { useCallback, useState } from 'react';
import { useKibana } from './common/lib/kibana';
export const useOnOpenCloseHandler = (): [boolean, () => void, () => void] => {
const [isOpen, setIsOpen] = useState(false);
@ -19,3 +20,12 @@ export const useOnOpenCloseHandler = (): [boolean, () => void, () => void] => {
}, []);
return [isOpen, handleOnOpen, handleOnClose];
};
/**
*
* @param capability Main Security feature capability name.
*/
export const useHasSecurityCapability = (capability: string): boolean => {
const { capabilities } = useKibana().services.application;
return !!capabilities.siem[capability];
};

View file

@ -53,7 +53,7 @@ const defaultProps = {
inspect: null,
refetch: () => {},
isModuleEnabled: true,
isLicenseValid: true,
isAuthorized: true,
loading: false,
};
const mockUseRiskScore = useRiskScore as jest.Mock;
@ -156,7 +156,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
{
'@timestamp': '1234567899',
[riskEntity]: {
name: 'testUsermame',
name: 'testUsername',
risk: {
rule_risks: [],
calculated_level: RiskSeverity.high,

View file

@ -111,7 +111,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
inspect,
refetch,
isDeprecated,
isLicenseValid,
isAuthorized,
isModuleEnabled,
} = useRiskScore({
filterQuery,
@ -140,7 +140,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
const refreshPage = useRefetchQueries();
if (!isLicenseValid) {
if (!isAuthorized) {
return null;
}

View file

@ -21,7 +21,7 @@ const defaultProps = {
inspect: null,
refetch: () => {},
isModuleEnabled: true,
isLicenseValid: true,
isAuthorized: true,
loading: true,
};
@ -88,7 +88,7 @@ describe('Host Summary Component', () => {
data: [
{
host: {
name: 'testHostmame',
name: 'testHostname',
risk: {
rule_risks: [],
calculated_score_norm: riskScore,

View file

@ -98,7 +98,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
}),
[from, to]
);
const { data: hostRisk, isLicenseValid } = useRiskScore({
const { data: hostRisk, isAuthorized } = useRiskScore({
filterQuery,
riskEntity: RiskScoreEntity.host,
skip: hostName == null,
@ -297,7 +297,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
)}
</OverviewWrapper>
</InspectButtonContainer>
{isLicenseValid && (
{isAuthorized && (
<HostRiskOverviewWrapper
gutterSize={isInDetailsSidePanel ? 'm' : 'none'}
direction={isInDetailsSidePanel ? 'column' : 'row'}

View file

@ -21,7 +21,7 @@ const defaultProps = {
inspect: null,
refetch: () => {},
isModuleEnabled: true,
isLicenseValid: true,
isAuthorized: true,
loading: false,
};
@ -101,7 +101,7 @@ describe('User Summary Component', () => {
data: [
{
user: {
name: 'testUsermame',
name: 'testUsername',
risk: {
rule_risks: [],
calculated_level: risk,

View file

@ -98,7 +98,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
[from, to]
);
const { data: userRisk, isLicenseValid } = useRiskScore({
const { data: userRisk, isAuthorized } = useRiskScore({
filterQuery,
skip: userName == null,
timerange,
@ -291,7 +291,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
)}
</OverviewWrapper>
</InspectButtonContainer>
{isLicenseValid && (
{isAuthorized && (
<UserRiskOverviewWrapper
gutterSize={isInDetailsSidePanel ? 'm' : 'none'}
direction={isInDetailsSidePanel ? 'column' : 'row'}

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}.entity-analytics`],
capabilities: [`${SERVER_APP_ID}.show`],
isBeta: false,
globalSearchKeywords: [ENTITY_ANALYTICS],
};

View file

@ -34,7 +34,7 @@ export const mockRiskScoreState = {
refetch: () => {},
totalCount: 0,
isModuleEnabled: true,
isLicenseValid: true,
isAuthorized: true,
isDeprecated: false,
loading: false,
};

View file

@ -27,7 +27,7 @@ describe('RiskScoreField', () => {
it('does not render content when the license is invalid', () => {
const { queryByTestId } = render(
<TestProviders>
<RiskScoreField riskScoreState={{ ...mockRiskScoreState, isLicenseValid: false }} />
<RiskScoreField riskScoreState={{ ...mockRiskScoreState, isAuthorized: false }} />
</TestProviders>
);

View file

@ -25,10 +25,10 @@ export const RiskScoreField = ({
}) => {
const { euiTheme } = useEuiTheme();
const { fontSize: xsFontSize } = useEuiFontSize('xs');
const { data: userRisk, isLicenseValid: isRiskLicenseValid } = riskScoreState;
const { data: userRisk, isAuthorized: isRiskScoreAuthorized } = riskScoreState;
const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined;
if (!isRiskLicenseValid) {
if (!isRiskScoreAuthorized) {
return null;
}

View file

@ -4,9 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { lazy } from 'react';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { AppFeatureKey } from '@kbn/security-solution-plugin/common';
import type { SecurityPageName, AppFeatureKey } from '@kbn/security-solution-plugin/common';
import type {
UpsellingService,
PageUpsellings,
@ -16,9 +14,6 @@ import type {
import type { SecurityProductTypes } from '../../common/config';
import { getProductAppFeatures } from '../../common/pli/pli_features';
const GenericUpsellingPageLazy = lazy(() => import('./pages/generic_upselling_page'));
const GenericUpsellingSectionLazy = lazy(() => import('./pages/generic_upselling_section'));
interface UpsellingsConfig {
pli: AppFeatureKey;
component: React.ComponentType;
@ -59,18 +54,22 @@ export const registerUpsellings = (
// Upsellings for entire pages, linked to a SecurityPageName
export const upsellingPages: UpsellingPages = [
{
pageName: SecurityPageName.entityAnalytics,
pli: AppFeatureKey.advancedInsights,
component: () => <GenericUpsellingPageLazy requiredPLI={AppFeatureKey.advancedInsights} />,
},
// 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} />,
// },
];
// Upsellings for sections, linked by arbitrary ids
export const upsellingSections: UpsellingSections = [
{
id: 'entity_analytics_panel',
pli: AppFeatureKey.advancedInsights,
component: () => <GenericUpsellingSectionLazy requiredPLI={AppFeatureKey.advancedInsights} />,
},
// 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} />,
// },
];