Skip to content

Commit

Permalink
[Security Solution] Allow Users to launch Timeline from the Entity An…
Browse files Browse the repository at this point in the history
…alytics dashboard (#143841)

* Add alerts count column to Entity Analytics risk tables
  • Loading branch information
machadoum authored Oct 25, 2022
1 parent 08331ad commit 227e045
Show file tree
Hide file tree
Showing 16 changed files with 334 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import type { IEsSearchRequest, IEsSearchResponse } from '@kbn/data-plugin/commo
import type { ESQuery } from '../../../../typed_json';

import type { Inspect, Maybe, SortField, TimerangeInput } from '../../../common';
import type { RiskScoreEntity } from '../common';

export interface RiskScoreRequestOptions extends IEsSearchRequest {
defaultIndex: string[];
riskScoreEntity: RiskScoreEntity;
timerange?: TimerangeInput;
includeAlertsCount?: boolean;
onlyLatest?: boolean;
pagination?: {
cursorStart: number;
Expand Down Expand Up @@ -47,6 +50,7 @@ export interface HostRiskScore {
name: string;
risk: RiskStats;
};
alertsCount?: number;
}

export interface UserRiskScore {
Expand All @@ -55,6 +59,7 @@ export interface UserRiskScore {
name: string;
risk: RiskStats;
};
alertsCount?: number;
}

export interface RuleRisk {
Expand All @@ -73,6 +78,7 @@ export const enum RiskScoreFields {
userName = 'user.name',
userRiskScore = 'user.risk.calculated_score_norm',
userRisk = 'user.risk.calculated_level',
alertsCount = 'alertsCount',
}

export interface RiskScoreItem {
Expand All @@ -85,6 +91,8 @@ export interface RiskScoreItem {

[RiskScoreFields.hostRiskScore]: Maybe<number>;
[RiskScoreFields.userRiskScore]: Maybe<number>;

[RiskScoreFields.alertsCount]: Maybe<number>;
}

export const enum RiskSeverity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,28 @@

import React from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import { EuiLink, EuiIcon, EuiToolTip } from '@elastic/eui';
import { get } from 'lodash/fp';
import { UsersTableType } from '../../../../users/store/model';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { HostDetailsLink, UserDetailsLink } from '../../../../common/components/links';
import { HostsTableType } from '../../../../hosts/store/model';
import { RiskScore } from '../../../../common/components/severity/common';
import type { HostRiskScore, RiskSeverity } from '../../../../../common/search_strategy';
import type {
HostRiskScore,
RiskSeverity,
UserRiskScore,
} from '../../../../../common/search_strategy';
import { RiskScoreEntity, RiskScoreFields } from '../../../../../common/search_strategy';
import * as i18n from './translations';
import { FormattedCount } from '../../../../common/components/formatted_number';

type HostRiskScoreColumns = Array<EuiBasicTableColumn<HostRiskScore>>;
type HostRiskScoreColumns = Array<EuiBasicTableColumn<HostRiskScore & UserRiskScore>>;

export const getRiskScoreColumns = (riskEntity: RiskScoreEntity): HostRiskScoreColumns => [
export const getRiskScoreColumns = (
riskEntity: RiskScoreEntity,
openEntityInTimeline: (entityName: string) => void
): HostRiskScoreColumns => [
{
field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
name: i18n.ENTITY_NAME(riskEntity),
Expand Down Expand Up @@ -75,4 +84,20 @@ export const getRiskScoreColumns = (riskEntity: RiskScoreEntity): HostRiskScoreC
return getEmptyTagValue();
},
},
{
field: RiskScoreFields.alertsCount,
width: '15%',
name: i18n.ALERTS,
truncateText: false,
mobileOptions: { show: true },
render: (alertCount: number, risk) => (
<EuiLink
data-test-subj="risk-score-alerts"
disabled={alertCount === 0}
onClick={() => openEntityInTimeline(get('host.name', risk) ?? get('user.name', risk))}
>
<FormattedCount count={alertCount} />
</EuiLink>
),
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { EntityAnalyticsRiskScores } from '.';
import type { UserRiskScore } from '../../../../../common/search_strategy';
import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy';
import type { SeverityCount } from '../../../../common/components/severity/types';
import { useRiskScore, useRiskScoreKpi } from '../../../../risk_score/containers';
Expand Down Expand Up @@ -116,5 +117,38 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(

expect(queryByTestId('entity_analytics_content')).not.toBeInTheDocument();
});

it('renders alerts count', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseRiskScoreKpi.mockReturnValue({
severityCount: mockSeverityCount,
loading: false,
});
const alertsCount = 999;
const data: UserRiskScore[] = [
{
'@timestamp': '1234567899',
user: {
name: 'testUsermame',
risk: {
rule_risks: [],
calculated_level: RiskSeverity.high,
calculated_score_norm: 75,
multipliers: [],
},
},
alertsCount,
},
];
mockUseRiskScore.mockReturnValue({ ...defaultProps, data });

const { queryByTestId } = render(
<TestProviders>
<EntityAnalyticsRiskScores riskEntity={riskEntity} />
</TestProviders>
);

expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString());
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import { useDispatch } from 'react-redux';
Expand Down Expand Up @@ -40,6 +40,7 @@ import { Loader } from '../../../../common/components/loader';
import { Panel } from '../../../../common/components/panel';
import * as commonI18n from '../common/translations';
import { usersActions } from '../../../../users/store';
import { useNavigateToTimeline } from '../../detection_response/hooks/use_navigate_to_timeline';

const HOST_RISK_TABLE_QUERY_ID = 'hostRiskDashboardTable';
const HOST_RISK_KPI_QUERY_ID = 'headerHostRiskScoreKpiQuery';
Expand Down Expand Up @@ -90,8 +91,24 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
[dispatch, riskEntity]
);

const { openHostInTimeline, openUserInTimeline } = useNavigateToTimeline();

const openEntityInTimeline = useCallback(
(entityName: string) => {
if (riskEntity === RiskScoreEntity.host) {
openHostInTimeline({ hostName: entityName });
} else if (riskEntity === RiskScoreEntity.user) {
openUserInTimeline({ userName: entityName });
}
},
[riskEntity, openHostInTimeline, openUserInTimeline]
);

const { toggleStatus, setToggleStatus } = useQueryToggle(entity.tableQueryId);
const columns = useMemo(() => getRiskScoreColumns(riskEntity), [riskEntity]);
const columns = useMemo(
() => getRiskScoreColumns(riskEntity, openEntityInTimeline),
[riskEntity, openEntityInTimeline]
);
const [selectedSeverity, setSelectedSeverity] = useState<RiskSeverity[]>([]);
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();

Expand Down Expand Up @@ -146,6 +163,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
},
timerange,
riskEntity,
includeAlertsCount: true,
});

useQueryInspector({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ export const getRiskEntityTranslation = (

return riskEntity === RiskScoreEntity.host ? HOST : USER;
};

export const ALERTS = i18n.translate('xpack.securitySolution.riskScore.overview.alerts', {
defaultMessage: 'Alerts',
});
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
expect(mockSearch).toHaveBeenCalledWith({
defaultIndex: [`ml_${riskEntity}_risk_score_latest_default`],
factoryQueryType: `${riskEntity}sRiskScore`,
riskScoreEntity: riskEntity,
includeAlertsCount: false,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface RiskScoreState<T extends RiskScoreEntity.host | RiskScoreEntity
export interface UseRiskScoreParams {
filterQuery?: ESQuery | string;
onlyLatest?: boolean;
includeAlertsCount?: boolean;
pagination?:
| {
cursorStart: number;
Expand Down Expand Up @@ -76,6 +77,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
skip = false,
pagination,
riskEntity,
includeAlertsCount = false,
}: UseRiskScore<T>): RiskScoreState<T> => {
const spaceId = useSpaceId();
const defaultIndex = spaceId
Expand Down Expand Up @@ -158,6 +160,8 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
? {
defaultIndex: [defaultIndex],
factoryQueryType,
riskScoreEntity: riskEntity,
includeAlertsCount,
filterQuery: createFilter(filterQuery),
pagination:
cursorStart !== undefined && querySize !== undefined
Expand All @@ -179,6 +183,8 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
sort,
requestTimerange,
onlyLatest,
riskEntity,
includeAlertsCount,
]
);

Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ export class Plugin implements ISecuritySolutionPlugin {
const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(
depsStart.data,
endpointContext,
depsStart.spaces?.spacesService?.getSpaceId
depsStart.spaces?.spacesService?.getSpaceId,
ruleDataClient
);

plugins.data.search.registerSearchStrategy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants';

import type { HostsRequestOptions } from '../../../../../../common/search_strategy/security_solution';
import { RiskScoreEntity } from '../../../../../../common/search_strategy/security_solution';
import * as buildQuery from './query.all_hosts.dsl';
import * as buildRiskQuery from '../../risk_score/all/query.risk_score.dsl';
import { allHosts } from '.';
Expand Down Expand Up @@ -128,6 +129,7 @@ describe('allHosts search strategy', () => {
expect(buildHostsRiskQuery).toHaveBeenCalledWith({
defaultIndex: ['ml_host_risk_score_latest_test-space'],
filterQuery: { terms: { 'host.name': [hostName] } },
riskScoreEntity: RiskScoreEntity.host,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import type {
} from '../../../../../../common/search_strategy/security_solution/hosts';

import type { HostRiskScore } from '../../../../../../common/search_strategy';
import { getHostRiskIndex, buildHostNamesFilter } from '../../../../../../common/search_strategy';
import {
RiskScoreEntity,
getHostRiskIndex,
buildHostNamesFilter,
} from '../../../../../../common/search_strategy';

import { inspectStringifyObject } from '../../../../../utils/build_query';
import type { SecuritySolutionFactory } from '../../types';
Expand Down Expand Up @@ -116,6 +120,7 @@ async function getHostRiskData(
buildRiskScoreQuery({
defaultIndex: [getHostRiskIndex(spaceId)],
filterQuery: buildHostNamesFilter(hostNames),
riskScoreEntity: RiskScoreEntity.host,
})
);
return hostRiskResponse;
Expand Down
Loading

0 comments on commit 227e045

Please sign in to comment.