Skip to content

Commit

Permalink
[Security Solution] Add risk score column to all users table (#142610)
Browse files Browse the repository at this point in the history
* Add risk score column to all users table

* [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs'

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
machadoum and kibanamachine authored Oct 6, 2022
1 parent b076e4b commit b93cf0f
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe, PageInfoPaginated } from '../../../common';
import type { RequestOptionsPaginated } from '../..';
import type { SortableUsersFields } from '../common';
import type { RiskSeverity } from '../../risk_score';

export interface User {
name: string;
lastSeen: string;
domain: string;
risk?: RiskSeverity;
}

export interface UsersStrategyResponse extends IEsSearchResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ import { TestProviders } from '../../../common/mock';

import { UsersTable } from '.';
import { usersModel } from '../../store';
import { Direction } from '../../../../common/search_strategy';
import { Direction, RiskSeverity } from '../../../../common/search_strategy';
import { UsersFields } from '../../../../common/search_strategy/security_solution/users/common';
import { render } from '@testing-library/react';

const mockUseMlCapabilities = jest.fn().mockReturnValue({ isPlatinumOrTrialLicense: false });

jest.mock('../../../common/components/ml/hooks/use_ml_capabilities', () => ({
useMlCapabilities: () => mockUseMlCapabilities(),
}));

describe('Users Table Component', () => {
const loadPage = jest.fn();

Expand Down Expand Up @@ -72,5 +78,73 @@ describe('Users Table Component', () => {

expect(getByTestId('table-allUsers-loading-false')).toHaveTextContent('(Empty string)');
});

test('it renders "Host Risk classfication" column when "isPlatinumOrTrialLicense" is truthy', () => {
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: true });

const { getAllByRole, getByText } = render(
<TestProviders>
<UsersTable
users={[
{
name: 'testUser',
lastSeen: '2019-04-08T18:35:45.064Z',
domain: 'test domain',
risk: RiskSeverity.critical,
},
]}
fakeTotalCount={50}
id="users"
loading={false}
loadPage={loadPage}
showMorePagesIndicator={false}
totalCount={0}
type={usersModel.UsersType.page}
sort={{
field: UsersFields.name,
direction: Direction.asc,
}}
setQuerySkip={() => {}}
/>
</TestProviders>
);

expect(getAllByRole('columnheader').length).toBe(4);
expect(getByText('Critical')).toBeInTheDocument();
});

test("it doesn't renders 'Host Risk classfication' column when 'isPlatinumOrTrialLicense' is falsy", () => {
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: false });

const { getAllByRole, queryByText } = render(
<TestProviders>
<UsersTable
users={[
{
name: 'testUser',
lastSeen: '2019-04-08T18:35:45.064Z',
domain: 'test domain',
risk: RiskSeverity.critical,
},
]}
fakeTotalCount={50}
id="users"
loading={false}
loadPage={loadPage}
showMorePagesIndicator={false}
totalCount={0}
type={usersModel.UsersType.page}
sort={{
field: UsersFields.name,
direction: Direction.asc,
}}
setQuerySkip={() => {}}
/>
</TestProviders>
);

expect(getAllByRole('columnheader').length).toBe(3);
expect(queryByText('Critical')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';

import { EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
import { UserDetailsLink } from '../../../common/components/links';
import { getOrEmptyTagFromValue } from '../../../common/components/empty_value';
import { getEmptyTagValue, getOrEmptyTagFromValue } from '../../../common/components/empty_value';

import type { Columns, Criteria, ItemsPerRow } from '../../../common/components/paginated_table';
import { PaginatedTable } from '../../../common/components/paginated_table';
Expand All @@ -22,6 +23,13 @@ import * as i18n from './translations';
import { usersActions, usersModel, usersSelectors } from '../../store';
import type { User } from '../../../../common/search_strategy/security_solution/users/all';
import type { SortUsersField } from '../../../../common/search_strategy/security_solution/users/common';
import type { RiskSeverity } from '../../../../common/search_strategy';
import { RiskScore } from '../../../common/components/severity/common';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { VIEW_USERS_BY_SEVERITY } from '../user_risk_score_table/translations';
import { SecurityPageName } from '../../../app/types';
import { UsersTableType } from '../../store/model';
import { useNavigateTo } from '../../../common/lib/kibana';

const tableType = usersModel.UsersTableType.allUsers;

Expand All @@ -41,7 +49,8 @@ interface UsersTableProps {
export type UsersTableColumns = [
Columns<User['name']>,
Columns<User['lastSeen']>,
Columns<User['domain']>
Columns<User['domain']>,
Columns<RiskSeverity>?
];

const rowItems: ItemsPerRow[] = [
Expand All @@ -55,51 +64,89 @@ const rowItems: ItemsPerRow[] = [
},
];

const getUsersColumns = (): UsersTableColumns => [
{
field: 'name',
name: i18n.USER_NAME,
truncateText: false,
sortable: true,
mobileOptions: { show: true },
render: (name) =>
name != null && name.length > 0
? getRowItemDraggables({
rowItems: [name],
attrName: 'user.name',
idPrefix: `users-table-${name}-name`,
render: (item) => <UserDetailsLink userName={item} />,
isAggregatable: true,
fieldType: 'keyword',
})
: getOrEmptyTagFromValue(name),
},
{
field: 'lastSeen',
name: i18n.LAST_SEEN,
sortable: true,
truncateText: false,
mobileOptions: { show: true },
render: (lastSeen) => <FormattedRelativePreferenceDate value={lastSeen} />,
},
{
field: 'domain',
name: i18n.DOMAIN,
sortable: false,
truncateText: false,
mobileOptions: { show: true },
render: (domain) =>
domain != null && domain.length > 0
? getRowItemDraggables({
rowItems: [domain],
attrName: 'user.domain',
idPrefix: `users-table-${domain}-domain`,
isAggregatable: true,
fieldType: 'keyword',
})
: getOrEmptyTagFromValue(domain),
},
];
const getUsersColumns = (
showRiskColumn: boolean,
dispatchSeverityUpdate: (s: RiskSeverity) => void
): UsersTableColumns => {
const columns: UsersTableColumns = [
{
field: 'name',
name: i18n.USER_NAME,
truncateText: false,
sortable: true,
mobileOptions: { show: true },
render: (name) =>
name != null && name.length > 0
? getRowItemDraggables({
rowItems: [name],
attrName: 'user.name',
idPrefix: `users-table-${name}-name`,
render: (item) => <UserDetailsLink userName={item} />,
isAggregatable: true,
fieldType: 'keyword',
})
: getOrEmptyTagFromValue(name),
},
{
field: 'lastSeen',
name: i18n.LAST_SEEN,
sortable: true,
truncateText: false,
mobileOptions: { show: true },
render: (lastSeen) => <FormattedRelativePreferenceDate value={lastSeen} />,
},
{
field: 'domain',
name: i18n.DOMAIN,
sortable: false,
truncateText: false,
mobileOptions: { show: true },
render: (domain) =>
domain != null && domain.length > 0
? getRowItemDraggables({
rowItems: [domain],
attrName: 'user.domain',
idPrefix: `users-table-${domain}-domain`,
isAggregatable: true,
fieldType: 'keyword',
})
: getOrEmptyTagFromValue(domain),
},
];

if (showRiskColumn) {
columns.push({
field: 'risk',
name: (
<EuiToolTip content={i18n.USER_RISK_TOOLTIP}>
<>
{i18n.USER_RISK} <EuiIcon color="subdued" type="iInCircle" className="eui-alignTop" />
</>
</EuiToolTip>
),
truncateText: false,
mobileOptions: { show: true },
sortable: false,
render: (riskScore: RiskSeverity) => {
if (riskScore != null) {
return (
<RiskScore
toolTipContent={
<EuiLink onClick={() => dispatchSeverityUpdate(riskScore)}>
<EuiText size="xs">{VIEW_USERS_BY_SEVERITY(riskScore.toLowerCase())}</EuiText>
</EuiLink>
}
severity={riskScore}
/>
);
}
return getEmptyTagValue();
},
});
}

return columns;
};

const UsersTableComponent: React.FC<UsersTableProps> = ({
users,
Expand All @@ -116,6 +163,8 @@ const UsersTableComponent: React.FC<UsersTableProps> = ({
const dispatch = useDispatch();
const getUsersSelector = useMemo(() => usersSelectors.allUsersSelector(), []);
const { activePage, limit } = useDeepEqualSelector((state) => getUsersSelector(state));
const isPlatinumOrTrialLicense = useMlCapabilities().isPlatinumOrTrialLicense;
const { navigateTo } = useNavigateTo();

const updateLimitPagination = useCallback(
(newLimit) => {
Expand Down Expand Up @@ -159,7 +208,26 @@ const UsersTableComponent: React.FC<UsersTableProps> = ({
},
[dispatch, sort]
);
const columns = useMemo(() => getUsersColumns(), []);

const dispatchSeverityUpdate = useCallback(
(s: RiskSeverity) => {
dispatch(
usersActions.updateUserRiskScoreSeverityFilter({
severitySelection: [s],
})
);
navigateTo({
deepLinkId: SecurityPageName.users,
path: UsersTableType.risk,
});
},
[dispatch, navigateTo]
);

const columns = useMemo(
() => getUsersColumns(isPlatinumOrTrialLicense, dispatchSeverityUpdate),
[isPlatinumOrTrialLicense, dispatchSeverityUpdate]
);

return (
<PaginatedTable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ export const UNIT = (totalCount: number) =>
values: { totalCount },
defaultMessage: `{totalCount, plural, =1 {user} other {users}}`,
});

export const USER_RISK_TOOLTIP = i18n.translate(
'xpack.securitySolution.usersTable.userRiskToolTip',
{
defaultMessage:
'User risk classification is determined by user risk score. Users classified as Critical or High are indicated as risky.',
}
);

export const USER_RISK = i18n.translate('xpack.securitySolution.usersTable.riskTitle', {
defaultMessage: 'User risk classification',
});
Loading

0 comments on commit b93cf0f

Please sign in to comment.