Skip to content

Commit

Permalink
[Security Solutions] Update risk score tables to filter by timerange (e…
Browse files Browse the repository at this point in the history
…lastic#168826)

issue: elastic#162451

## Summary

* Update Entity analytics dashboard to filter by timerange and to
display timestamp field
* Update Users risk score tab to filter by timerange and to display
timestamp field
* Update Hosts risk score tab to filter by timerange and to display
timestamp field
* Delete tooltip that used to warn users that risk tables din't filter
by timerange

<img width="1501" alt="Screenshot 2023-10-13 at 11 54 19"
src="https://github.com/elastic/kibana/assets/1490444/a99e6ec7-0cbd-44a9-b1b1-b2dc9f4ad7cf">
<img width="1506" alt="Screenshot 2023-10-13 at 11 54 38"
src="https://github.com/elastic/kibana/assets/1490444/78f59c54-9210-4d09-8e22-bdab1b2103c5">
<img width="1497" alt="Screenshot 2023-10-13 at 11 54 53"
src="https://github.com/elastic/kibana/assets/1490444/35c19ee4-3cbc-42f5-96c1-1c63dc47300b">

### How to test
* Create alerts and enable the risk engine
* Check if the Entity analytics dashboard filters by timerange
* Check if the Users risk score tab filters by timerange
* Check if the Hosts risk score tab filters by timerange
* Check if the risk score on top of the user details page does NOT
filter by timerange
* Check if the risk score inside flyouts (Alerts, users and hosts) does
NOT filter by timerange
* Check if the info tooltips about the timerange filter were removed

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

(cherry picked from commit e12cfc1)

# Conflicts:
#	x-pack/plugins/security_solution/public/explore/containers/risk_score/kpi/index.tsx
#	x-pack/test/security_solution_cypress/cypress/e2e/explore/dashboards/entity_analytics.cy.ts
  • Loading branch information
machadoum committed Oct 23, 2023
1 parent 9a37cd5 commit 08680f7
Show file tree
Hide file tree
Showing 27 changed files with 171 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export interface RiskScoreItem {
[RiskScoreFields.hostName]: Maybe<string>;
[RiskScoreFields.userName]: Maybe<string>;

[RiskScoreFields.timestamp]: Maybe<string>;

[RiskScoreFields.hostRisk]: Maybe<RiskSeverity>;
[RiskScoreFields.userRisk]: Maybe<RiskSeverity>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { CommonFields, Maybe, RiskScoreFields, RiskSeverity, SortField } fr
export interface UserRiskScoreItem {
_id?: Maybe<string>;
[RiskScoreFields.userName]: Maybe<string>;
[RiskScoreFields.timestamp]: Maybe<string>;
[RiskScoreFields.userRisk]: Maybe<RiskSeverity>;
[RiskScoreFields.userRiskScore]: Maybe<number>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import * as i18n from './translations';
import { RiskScoreHeaderTitle } from './risk_score_header_title';
import { RiskScoreRestartButton } from './risk_score_restart_button';
import type { inputsModel } from '../../../../common/store';
import * as overviewI18n from '../../../../overview/components/entity_analytics/common/translations';
import { useIsNewRiskScoreModuleInstalled } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';

const RiskScoresNoDataDetectedComponent = ({
Expand All @@ -37,15 +36,7 @@ const RiskScoresNoDataDetectedComponent = ({

return (
<EuiPanel data-test-subj={`${entityType}-risk-score-no-data-detected`} hasBorder>
<HeaderSection
title={<RiskScoreHeaderTitle riskScoreEntity={entityType} />}
titleSize="s"
tooltip={
entityType === RiskScoreEntity.user
? overviewI18n.USER_RISK_TABLE_TOOLTIP
: overviewI18n.HOST_RISK_TABLE_TOOLTIP
}
/>
<HeaderSection title={<RiskScoreHeaderTitle riskScoreEntity={entityType} />} titleSize="s" />
<EuiEmptyPrompt
title={<h2>{translations.title}</h2>}
body={translations.body}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,6 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
}
}, [defaultIndex, refetch, refetchDeprecated]);

// since query does not take timerange arg, we need to manually refetch when time range updates
// the results can be different if the user has run the ML for the first time since pressing refresh
useEffect(() => {
refetchAll();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timerange?.to, timerange?.from]);

const riskScoreResponse = useMemo(
() => ({
data: response.data,
Expand Down Expand Up @@ -168,7 +161,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
}
: undefined,
sort,
timerange: onlyLatest ? undefined : requestTimerange,
timerange: requestTimerange,
alertsTimerange: includeAlertsCount ? requestTimerange : undefined,
}
: null,
Expand All @@ -180,7 +173,6 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
querySize,
sort,
requestTimerange,
onlyLatest,
riskEntity,
includeAlertsCount,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { InspectResponse } from '../../../../types';
import type { inputsModel } from '../../../../common/store';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useIsNewRiskScoreModuleInstalled } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
import { useRiskScoreFeatureStatus } from '../feature_status';

interface RiskScoreKpi {
error: unknown;
Expand Down Expand Up @@ -60,6 +61,14 @@ export const useRiskScoreKpi = ({
: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled)
: undefined;

const {
isDeprecated,
isEnabled,
isAuthorized,
isLoading: isDeprecatedLoading,
refetch: refetchFeatureStatus,
} = useRiskScoreFeatureStatus(riskEntity, defaultIndex);

const { loading, result, search, refetch, inspect, error } =
useSearchStrategy<RiskQueries.kpiRiskScore>({
factoryQueryType: RiskQueries.kpiRiskScore,
Expand All @@ -72,21 +81,39 @@ export const useRiskScoreKpi = ({

const isModuleDisabled = !!error && isIndexNotFoundError(error);

const requestTimerange = useMemo(
() => (timerange ? { to: timerange.to, from: timerange.from, interval: '' } : undefined),
[timerange]
);

useEffect(() => {
if (!skip && defaultIndex && featureEnabled) {
search({
filterQuery,
defaultIndex: [defaultIndex],
entity: riskEntity,
timerange: requestTimerange,
});
}
}, [defaultIndex, search, filterQuery, skip, riskEntity, featureEnabled]);
}, [
defaultIndex,
search,
filterQuery,
skip,
riskEntity,
requestTimerange,
isEnabled,
isDeprecated,
isAuthorized,
isDeprecatedLoading,
]);

// since query does not take timerange arg, we need to manually refetch when time range updates
useEffect(() => {
refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timerange?.to, timerange?.from]);
const refetchAll = useCallback(() => {
if (defaultIndex) {
refetchFeatureStatus(defaultIndex);
refetch();
}
}, [defaultIndex, refetch, refetchFeatureStatus]);

useEffect(() => {
if (error) {
Expand All @@ -110,5 +137,9 @@ export const useRiskScoreKpi = ({
};
}, [result, loading, error]);

return { error, severityCount, loading, isModuleDisabled, refetch, inspect };
return { error, severityCount, loading, isModuleDisabled, refetch: refetchAll, inspect };
};
function useCallback(arg0: () => void, arg1: any[]) {
throw new Error('Function not implemented.');
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('getHostRiskScoreColumns', () => {
});

const riskScore = 10.11111111;
const riskScoreColumn = columns[1];
const riskScoreColumn = columns[2];
const renderedColumn = riskScoreColumn.render!(riskScore, null);

const { queryByTestId } = render(<TestProviders>{renderedColumn}</TestProviders>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { HostDetailsLink } from '../../../../common/components/links';
import type { HostRiskScoreColumns } from '.';
import * as i18n from './translations';
import { HostsTableType } from '../../store/model';
import type { RiskSeverity } from '../../../../../common/search_strategy';
import type { Maybe, RiskSeverity } from '../../../../../common/search_strategy';
import { RiskScoreFields, RiskScoreEntity } from '../../../../../common/search_strategy';
import { RiskScoreLevel } from '../../../components/risk_score/severity/common';
import { ENTITY_RISK_LEVEL } from '../../../components/risk_score/translations';
import { CELL_ACTIONS_TELEMETRY } from '../../../components/risk_score/constants';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';

export const getHostRiskScoreColumns = ({
dispatchSeverityUpdate,
Expand All @@ -34,6 +35,7 @@ export const getHostRiskScoreColumns = ({
truncateText: false,
mobileOptions: { show: true },
sortable: true,
width: '35%',
render: (hostName) => {
if (hostName != null && hostName.length > 0) {
return (
Expand All @@ -57,6 +59,19 @@ export const getHostRiskScoreColumns = ({
return getEmptyTagValue();
},
},
{
field: RiskScoreFields.timestamp,
name: i18n.LAST_UPDATED,
truncateText: false,
mobileOptions: { show: true },
sortable: true,
render: (lastSeen: Maybe<string>) => {
if (lastSeen != null) {
return <FormattedRelativePreferenceDate value={lastSeen} />;
}
return getEmptyTagValue();
},
},
{
field: RiskScoreFields.hostRiskScore,
name: i18n.HOST_RISK_SCORE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ interface HostRiskScoreTableProps {

export type HostRiskScoreColumns = [
Columns<RiskScoreItem[RiskScoreFields.hostName]>,
Columns<RiskScoreItem[RiskScoreFields.timestamp]>,
Columns<RiskScoreItem[RiskScoreFields.hostRiskScore]>,
Columns<RiskScoreItem[RiskScoreFields.hostRisk]>
];
Expand Down Expand Up @@ -191,7 +192,6 @@ const HostRiskScoreTableComponent: React.FC<HostRiskScoreTableProps> = ({
headerSupplement={risk}
headerTitle={i18nHosts.HOST_RISK_TITLE}
headerUnit={i18n.UNIT(totalCount)}
headerTooltip={i18nHosts.HOST_RISK_TABLE_TOOLTIP}
id={id}
isInspect={isInspect}
itemsPerRow={rowItems}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,15 @@ export const HOST_RISK_TITLE = i18n.translate(
}
);

export const HOST_RISK_TABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.hostsRiskTable.hostsTableTooltip',
{
defaultMessage:
'The host risk table is not affected by the KQL time range. This table shows the latest recorded risk score for each host.',
}
);

export const VIEW_HOSTS_BY_SEVERITY = (severity: string) =>
i18n.translate('xpack.securitySolution.hostsRiskTable.filteredHostsTitle', {
values: { severity },
defaultMessage: 'View {severity} risk hosts',
});

export const LAST_UPDATED = i18n.translate(
'xpack.securitySolution.hostsRiskTable.lastUpdatedTitle',
{
defaultMessage: 'Last updated',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ describe('getUserRiskScoreColumns', () => {
const columns = getUserRiskScoreColumns(defaultProps);

expect(columns[0].field).toBe('user.name');
expect(columns[1].field).toBe(RiskScoreFields.userRiskScore);
expect(columns[2].field).toBe(RiskScoreFields.userRisk);
expect(columns[1].field).toBe(RiskScoreFields.timestamp);
expect(columns[2].field).toBe(RiskScoreFields.userRiskScore);
expect(columns[3].field).toBe(RiskScoreFields.userRisk);

columns.forEach((column) => {
expect(column).toHaveProperty('name');
Expand All @@ -45,7 +46,7 @@ describe('getUserRiskScoreColumns', () => {
const columns: UserRiskScoreColumns = getUserRiskScoreColumns(defaultProps);

const riskScore = 10.11111111;
const riskScoreColumn = columns[1];
const riskScoreColumn = columns[2];
const renderedColumn = riskScoreColumn.render!(riskScore, null);

const { queryByTestId } = render(<TestProviders>{renderedColumn}</TestProviders>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value';
import type { UserRiskScoreColumns } from '.';
import * as i18n from './translations';
import { RiskScoreLevel } from '../../../components/risk_score/severity/common';
import type { RiskSeverity } from '../../../../../common/search_strategy';
import type { Maybe, RiskSeverity } from '../../../../../common/search_strategy';
import { RiskScoreEntity, RiskScoreFields } from '../../../../../common/search_strategy';
import { UserDetailsLink } from '../../../../common/components/links';
import { UsersTableType } from '../../store/model';
import { ENTITY_RISK_LEVEL } from '../../../components/risk_score/translations';
import { CELL_ACTIONS_TELEMETRY } from '../../../components/risk_score/constants';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';

export const getUserRiskScoreColumns = ({
dispatchSeverityUpdate,
Expand All @@ -35,6 +36,7 @@ export const getUserRiskScoreColumns = ({
truncateText: false,
mobileOptions: { show: true },
sortable: true,
width: '35%',
render: (userName) => {
if (userName != null && userName.length > 0) {
const id = escapeDataProviderId(`user-risk-score-table-userName-${userName}`);
Expand All @@ -60,6 +62,19 @@ export const getUserRiskScoreColumns = ({
return getEmptyTagValue();
},
},
{
field: RiskScoreFields.timestamp,
name: i18n.LAST_UPDATED,
truncateText: false,
mobileOptions: { show: true },
sortable: true,
render: (lastSeen: Maybe<string>) => {
if (lastSeen != null) {
return <FormattedRelativePreferenceDate value={lastSeen} />;
}
return getEmptyTagValue();
},
},
{
field: RiskScoreFields.userRiskScore,
name: i18n.USER_RISK_SCORE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ interface UserRiskScoreTableProps {

export type UserRiskScoreColumns = [
Columns<UserRiskScoreItem[RiskScoreFields.userName]>,
Columns<UserRiskScoreItem[RiskScoreFields.timestamp]>,
Columns<UserRiskScoreItem[RiskScoreFields.userRiskScore]>,
Columns<UserRiskScoreItem[RiskScoreFields.userRisk]>
];
Expand Down Expand Up @@ -191,7 +192,6 @@ const UserRiskScoreTableComponent: React.FC<UserRiskScoreTableProps> = ({
}
headerSupplement={risk}
headerTitle={i18nUsers.NAVIGATION_RISK_TITLE}
headerTooltip={i18n.USER_RISK_TABLE_TOOLTIP}
headerUnit={i18n.UNIT(totalCount)}
id={id}
isInspect={isInspect}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ export const ROWS_10 = i18n.translate('xpack.securitySolution.usersTable.rows',
defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}',
});

export const USER_RISK_TABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.hostsRiskTable.usersTableTooltip',
{
defaultMessage:
'The user risk table is not affected by the KQL time range. This table shows the latest recorded risk score for each user.',
}
);
export const LAST_UPDATED = i18n.translate('xpack.securitySolution.usersTable.lastUpdatedTitle', {
defaultMessage: 'Last updated',
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,3 @@ export const USER_RISK_TITLE = i18n.translate(
defaultMessage: 'User Risk Scores',
}
);

export const HOST_RISK_TABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip',
{
defaultMessage:
'The host risk table is not affected by the time range. This table shows the latest recorded risk score for each host.',
}
);

export const USER_RISK_TABLE_TOOLTIP = i18n.translate(
'xpack.securitySolution.entityAnalytics.usersRiskDashboard.usersTableTooltip',
{
defaultMessage:
'The user risk table is not affected by the time range. This table shows the latest recorded risk score for each user.',
}
);
Loading

0 comments on commit 08680f7

Please sign in to comment.