* Filter out empty values from alert flyout overview * Add Host Risk metadata data to alerts flyout * Add feature flag to host risk data query * Swap investigation guide and enrichment data places in the UI * Migrate alert_summary_view.test to react testing library * Refactor threat summary by extracting components and renaming Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
parent
c07e45232e
commit
bbaec75321
|
@ -333,7 +333,7 @@ export const ELASTIC_NAME = 'estc';
|
|||
|
||||
export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`;
|
||||
|
||||
export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest';
|
||||
export const HOST_RISK_SCORES_INDEX = 'ml_host_risk_score_latest';
|
||||
|
||||
export const TRANSFORM_STATES = {
|
||||
ABORTING: 'aborting',
|
||||
|
|
|
@ -11,7 +11,7 @@ export * from './common';
|
|||
export * from './details';
|
||||
export * from './first_last_seen';
|
||||
export * from './kpi';
|
||||
export * from './risky_hosts';
|
||||
export * from './risk_score';
|
||||
export * from './overview';
|
||||
export * from './uncommon_processes';
|
||||
|
||||
|
@ -23,6 +23,6 @@ export enum HostsQueries {
|
|||
hosts = 'hosts',
|
||||
hostsEntities = 'hostsEntities',
|
||||
overview = 'overviewHost',
|
||||
riskyHosts = 'riskyHosts',
|
||||
hostsRiskScore = 'hostsRiskScore',
|
||||
uncommonProcesses = 'uncommonProcesses',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { FactoryQueryTypes } from '../..';
|
||||
import {
|
||||
IEsSearchRequest,
|
||||
IEsSearchResponse,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { Inspect, Maybe, TimerangeInput } from '../../../common';
|
||||
|
||||
export interface HostsRiskScoreRequestOptions extends IEsSearchRequest {
|
||||
defaultIndex: string[];
|
||||
factoryQueryType?: FactoryQueryTypes;
|
||||
hostName?: string;
|
||||
timerange?: TimerangeInput;
|
||||
}
|
||||
|
||||
export interface HostsRiskScoreStrategyResponse extends IEsSearchResponse {
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export interface HostsRiskScore {
|
||||
host: {
|
||||
name: string;
|
||||
};
|
||||
risk_score: number;
|
||||
risk: string;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* 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 { Inspect, Maybe, RequestBasicOptions } from '../../..';
|
||||
import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
|
||||
|
||||
export type HostsRiskyHostsRequestOptions = RequestBasicOptions;
|
||||
|
||||
export interface HostsRiskyHostsStrategyResponse extends IEsSearchResponse {
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
|
@ -28,8 +28,8 @@ import {
|
|||
HostsKpiUniqueIpsStrategyResponse,
|
||||
HostsKpiUniqueIpsRequestOptions,
|
||||
HostFirstLastSeenRequestOptions,
|
||||
HostsRiskyHostsStrategyResponse,
|
||||
HostsRiskyHostsRequestOptions,
|
||||
HostsRiskScoreStrategyResponse,
|
||||
HostsRiskScoreRequestOptions,
|
||||
} from './hosts';
|
||||
import {
|
||||
NetworkQueries,
|
||||
|
@ -126,8 +126,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
|
|||
? HostDetailsStrategyResponse
|
||||
: T extends UebaQueries.riskScore
|
||||
? RiskScoreStrategyResponse
|
||||
: T extends HostsQueries.riskyHosts
|
||||
? HostsRiskyHostsStrategyResponse
|
||||
: T extends HostsQueries.hostsRiskScore
|
||||
? HostsRiskScoreStrategyResponse
|
||||
: T extends UebaQueries.hostRules
|
||||
? HostRulesStrategyResponse
|
||||
: T extends UebaQueries.userRules
|
||||
|
@ -182,8 +182,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
|
|||
|
||||
export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQueries.hosts
|
||||
? HostsRequestOptions
|
||||
: T extends HostsQueries.riskyHosts
|
||||
? HostsRiskyHostsRequestOptions
|
||||
: T extends HostsQueries.hostsRiskScore
|
||||
? HostsRiskScoreRequestOptions
|
||||
: T extends HostsQueries.details
|
||||
? HostDetailsRequestOptions
|
||||
: T extends HostsQueries.overview
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AlertSummaryView Behavior event code renders additional summary rows 1`] = `
|
||||
.c1 {
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
|
||||
.c0 .euiTableHeaderCell,
|
||||
.c0 .euiTableRowCell {
|
||||
border: none;
|
||||
|
@ -24,6 +20,10 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
min-width: 138px;
|
||||
padding: 0 8px;
|
||||
|
@ -53,10 +53,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
<div
|
||||
class="euiBasicTable c0"
|
||||
data-test-subj="summary-view"
|
||||
|
@ -141,7 +137,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.status"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -205,7 +203,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-@timestamp"
|
||||
>
|
||||
<div
|
||||
class="eventFieldsTable__fieldValue"
|
||||
>
|
||||
|
@ -273,7 +273,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.rule.name"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -337,7 +339,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.rule.severity"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -401,7 +405,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.rule.risk_score"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -465,7 +471,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-host.name"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -529,7 +537,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-user.name"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -593,7 +603,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-source.ip"
|
||||
>
|
||||
<div
|
||||
class="eventFieldsTable__fieldValue"
|
||||
>
|
||||
|
@ -644,161 +656,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
destination.ip
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Threshold Count
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Threshold Terms
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Threshold Cardinality
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Rule description
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -806,10 +663,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
`;
|
||||
|
||||
exports[`AlertSummaryView Memory event code renders additional summary rows 1`] = `
|
||||
.c1 {
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
|
||||
.c0 .euiTableHeaderCell,
|
||||
.c0 .euiTableRowCell {
|
||||
border: none;
|
||||
|
@ -829,6 +682,10 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
line-height: 1.7rem;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
min-width: 138px;
|
||||
padding: 0 8px;
|
||||
|
@ -858,10 +715,6 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
<div
|
||||
class="euiBasicTable c0"
|
||||
data-test-subj="summary-view"
|
||||
|
@ -946,7 +799,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.status"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -1010,7 +865,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-@timestamp"
|
||||
>
|
||||
<div
|
||||
class="eventFieldsTable__fieldValue"
|
||||
>
|
||||
|
@ -1078,7 +935,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.rule.name"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -1142,7 +1001,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.rule.severity"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -1206,7 +1067,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-signal.rule.risk_score"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -1270,7 +1133,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-host.name"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -1334,7 +1199,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-user.name"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--extraSmall"
|
||||
>
|
||||
|
@ -1398,7 +1265,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
data-test-subj="event-field-source.ip"
|
||||
>
|
||||
<div
|
||||
class="eventFieldsTable__fieldValue"
|
||||
>
|
||||
|
@ -1449,192 +1318,6 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
destination.ip
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Threshold Count
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Threshold Terms
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Threshold Cardinality
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Rule name
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="euiTableRow"
|
||||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
style="width: 220px;"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<h5
|
||||
class="c1 euiTitle euiTitle--xxxsmall"
|
||||
>
|
||||
Import Hash
|
||||
</h5>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { waitFor, render } from '@testing-library/react';
|
||||
|
||||
import { AlertSummaryView } from './alert_summary_view';
|
||||
import { mockAlertDetailsData } from './__mocks__';
|
||||
|
@ -15,7 +15,6 @@ import { useRuleWithFallback } from '../../../detections/containers/detection_en
|
|||
|
||||
import { TestProviders, TestProvidersComponent } from '../../mock';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
|
@ -33,8 +32,6 @@ const props = {
|
|||
};
|
||||
|
||||
describe('AlertSummaryView', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useRuleWithFallback as jest.Mock).mockReturnValue({
|
||||
|
@ -44,23 +41,12 @@ describe('AlertSummaryView', () => {
|
|||
});
|
||||
});
|
||||
test('render correct items', () => {
|
||||
const wrapper = mount(
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AlertSummaryView {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('render investigation guide', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AlertSummaryView {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true);
|
||||
});
|
||||
expect(getByTestId('summary-view')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("render no investigation guide if it doesn't exist", async () => {
|
||||
|
@ -69,13 +55,13 @@ describe('AlertSummaryView', () => {
|
|||
note: null,
|
||||
},
|
||||
});
|
||||
const wrapper = mount(
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<AlertSummaryView {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(false);
|
||||
expect(queryByTestId('summary-view-guide')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
test('Memory event code renders additional summary rows', () => {
|
||||
|
@ -93,12 +79,12 @@ describe('AlertSummaryView', () => {
|
|||
return item;
|
||||
}) as TimelineEventsDetailsItem[],
|
||||
};
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<TestProvidersComponent>
|
||||
<AlertSummaryView {...renderProps} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(wrapper.find('div[data-test-subj="summary-view"]').render()).toMatchSnapshot();
|
||||
expect(container.querySelector('div[data-test-subj="summary-view"]')).toMatchSnapshot();
|
||||
});
|
||||
test('Behavior event code renders additional summary rows', () => {
|
||||
const renderProps = {
|
||||
|
@ -115,11 +101,36 @@ describe('AlertSummaryView', () => {
|
|||
return item;
|
||||
}) as TimelineEventsDetailsItem[],
|
||||
};
|
||||
const wrapper = mount(
|
||||
const { container } = render(
|
||||
<TestProvidersComponent>
|
||||
<AlertSummaryView {...renderProps} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
expect(wrapper.find('div[data-test-subj="summary-view"]').render()).toMatchSnapshot();
|
||||
expect(container.querySelector('div[data-test-subj="summary-view"]')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("doesn't render empty fields", () => {
|
||||
const renderProps = {
|
||||
...props,
|
||||
data: mockAlertDetailsData.map((item) => {
|
||||
if (item.category === 'signal' && item.field === 'signal.rule.name') {
|
||||
return {
|
||||
category: 'signal',
|
||||
field: 'signal.rule.name',
|
||||
values: undefined,
|
||||
originalValue: undefined,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}) as TimelineEventsDetailsItem[],
|
||||
};
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<AlertSummaryView {...renderProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('event-field-signal.rule.name')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,113 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBasicTableColumn, EuiSpacer, EuiHorizontalRule, EuiTitle, EuiText } from '@elastic/eui';
|
||||
import { get, getOr, find, isEmpty } from 'lodash/fp';
|
||||
import { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import {
|
||||
ALERTS_HEADERS_RISK_SCORE,
|
||||
ALERTS_HEADERS_RULE,
|
||||
ALERTS_HEADERS_SEVERITY,
|
||||
ALERTS_HEADERS_THRESHOLD_CARDINALITY,
|
||||
ALERTS_HEADERS_THRESHOLD_COUNT,
|
||||
ALERTS_HEADERS_THRESHOLD_TERMS,
|
||||
ALERTS_HEADERS_RULE_NAME,
|
||||
SIGNAL_STATUS,
|
||||
ALERTS_HEADERS_TARGET_IMPORT_HASH,
|
||||
TIMESTAMP,
|
||||
ALERTS_HEADERS_RULE_DESCRIPTION,
|
||||
} from '../../../detections/components/alerts_table/translations';
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
IP_FIELD_TYPE,
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip';
|
||||
import { SummaryView } from './summary_view';
|
||||
import { AlertSummaryRow, getSummaryColumns, SummaryRow } from './helpers';
|
||||
import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback';
|
||||
import { MarkdownRenderer } from '../markdown_editor';
|
||||
import { LineClamp } from '../line_clamp';
|
||||
import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check';
|
||||
import { getEmptyValue } from '../empty_value';
|
||||
|
||||
import { ActionCell } from './table/action_cell';
|
||||
import { FieldValueCell } from './table/field_value_cell';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common';
|
||||
import { EventCode } from '../../../../common/ecs/event';
|
||||
|
||||
export const Indent = styled.div`
|
||||
padding: 0 8px;
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
const StyledEmptyComponent = styled.div`
|
||||
padding: ${(props) => `${props.theme.eui.paddingSizes.xs} 0`};
|
||||
`;
|
||||
|
||||
interface EventSummaryField {
|
||||
id: string;
|
||||
label?: string;
|
||||
linkField?: string;
|
||||
fieldType?: string;
|
||||
overrideField?: string;
|
||||
}
|
||||
|
||||
const defaultDisplayFields: EventSummaryField[] = [
|
||||
{ id: 'signal.status', label: SIGNAL_STATUS },
|
||||
{ id: '@timestamp', label: TIMESTAMP },
|
||||
{
|
||||
id: SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
linkField: 'signal.rule.id',
|
||||
label: ALERTS_HEADERS_RULE,
|
||||
},
|
||||
{ id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY },
|
||||
{ id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE },
|
||||
{ id: 'host.name' },
|
||||
{ id: 'agent.id', overrideField: AGENT_STATUS_FIELD_NAME, label: i18n.AGENT_STATUS },
|
||||
{ id: 'user.name' },
|
||||
{ id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
|
||||
{ id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
|
||||
{ id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT },
|
||||
{ id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS },
|
||||
{ id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY },
|
||||
];
|
||||
|
||||
const processCategoryFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'process.name' },
|
||||
{ id: 'process.parent.name' },
|
||||
{ id: 'process.args' },
|
||||
];
|
||||
|
||||
const networkCategoryFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'destination.address' },
|
||||
{ id: 'destination.port' },
|
||||
{ id: 'process.name' },
|
||||
];
|
||||
|
||||
const memoryShellCodeAlertFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME },
|
||||
{
|
||||
id: 'Target.process.thread.Ext.start_address_details.memory_pe.imphash',
|
||||
label: ALERTS_HEADERS_TARGET_IMPORT_HASH,
|
||||
},
|
||||
];
|
||||
|
||||
const behaviorAlertFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'rule.description', label: ALERTS_HEADERS_RULE_DESCRIPTION },
|
||||
];
|
||||
|
||||
const memorySignatureAlertFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME },
|
||||
];
|
||||
import { getSummaryRows } from './get_alert_summary_rows';
|
||||
|
||||
const getDescription = ({
|
||||
data,
|
||||
|
@ -121,187 +26,28 @@ const getDescription = ({
|
|||
linkValue,
|
||||
timelineId,
|
||||
values,
|
||||
}: AlertSummaryRow['description']) => {
|
||||
if (isEmpty(values)) {
|
||||
return <StyledEmptyComponent>{getEmptyValue()}</StyledEmptyComponent>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldValueCell
|
||||
contextId={timelineId}
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
linkValue={linkValue}
|
||||
isDraggable={isDraggable}
|
||||
values={values}
|
||||
/>
|
||||
<ActionCell
|
||||
contextId={timelineId}
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
linkValue={linkValue}
|
||||
timelineId={timelineId}
|
||||
values={values}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getEventFieldsToDisplay({
|
||||
eventCategory,
|
||||
eventCode,
|
||||
}: {
|
||||
eventCategory: string;
|
||||
eventCode?: string;
|
||||
}): EventSummaryField[] {
|
||||
switch (eventCode) {
|
||||
// memory protection fields
|
||||
case EventCode.SHELLCODE_THREAD:
|
||||
return memoryShellCodeAlertFields;
|
||||
case EventCode.MEMORY_SIGNATURE:
|
||||
return memorySignatureAlertFields;
|
||||
case EventCode.BEHAVIOR:
|
||||
return behaviorAlertFields;
|
||||
}
|
||||
|
||||
switch (eventCategory) {
|
||||
case 'network':
|
||||
return networkCategoryFields;
|
||||
|
||||
case 'process':
|
||||
return processCategoryFields;
|
||||
}
|
||||
|
||||
return defaultDisplayFields;
|
||||
}
|
||||
|
||||
export const getSummaryRows = ({
|
||||
data,
|
||||
browserFields,
|
||||
timelineId,
|
||||
eventId,
|
||||
isDraggable = false,
|
||||
}: {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
browserFields: BrowserFields;
|
||||
timelineId: string;
|
||||
eventId: string;
|
||||
isDraggable?: boolean;
|
||||
}) => {
|
||||
const eventCategoryField = find({ category: 'event', field: 'event.category' }, data);
|
||||
|
||||
const eventCategory = Array.isArray(eventCategoryField?.originalValue)
|
||||
? eventCategoryField?.originalValue[0]
|
||||
: eventCategoryField?.originalValue;
|
||||
|
||||
const eventCodeField = find({ category: 'event', field: 'event.code' }, data);
|
||||
|
||||
const eventCode = Array.isArray(eventCodeField?.originalValue)
|
||||
? eventCodeField?.originalValue?.[0]
|
||||
: eventCodeField?.originalValue;
|
||||
|
||||
const tableFields = getEventFieldsToDisplay({ eventCategory, eventCode });
|
||||
|
||||
return data != null
|
||||
? tableFields.reduce<SummaryRow[]>((acc, item) => {
|
||||
const initialDescription = {
|
||||
contextId: timelineId,
|
||||
eventId,
|
||||
isDraggable,
|
||||
value: null,
|
||||
fieldType: 'string',
|
||||
linkValue: undefined,
|
||||
timelineId,
|
||||
};
|
||||
const field = data.find((d) => d.field === item.id);
|
||||
if (!field) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: item.label ?? item.id,
|
||||
description: initialDescription,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const linkValueField =
|
||||
item.linkField != null && data.find((d) => d.field === item.linkField);
|
||||
const linkValue = getOr(null, 'originalValue.0', linkValueField);
|
||||
const value = getOr(null, 'originalValue.0', field);
|
||||
const category = field.category ?? '';
|
||||
const fieldName = field.field ?? '';
|
||||
|
||||
const browserField = get([category, 'fields', fieldName], browserFields);
|
||||
const description = {
|
||||
...initialDescription,
|
||||
data: {
|
||||
field: field.field,
|
||||
format: browserField?.format ?? '',
|
||||
type: browserField?.type ?? '',
|
||||
isObjectArray: field.isObjectArray,
|
||||
...(item.overrideField ? { field: item.overrideField } : {}),
|
||||
},
|
||||
values: field.values,
|
||||
linkValue: linkValue ?? undefined,
|
||||
fieldFromBrowserField: browserField,
|
||||
};
|
||||
|
||||
if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (item.id === 'signal.threshold_result.terms') {
|
||||
try {
|
||||
const terms = getOr(null, 'originalValue', field);
|
||||
const parsedValue = terms.map((term: string) => JSON.parse(term));
|
||||
const thresholdTerms = (parsedValue ?? []).map(
|
||||
(entry: { field: string; value: string }) => {
|
||||
return {
|
||||
title: `${entry.field} [threshold]`,
|
||||
description: {
|
||||
...description,
|
||||
values: [entry.value],
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
return [...acc, ...thresholdTerms];
|
||||
} catch (err) {
|
||||
return [...acc];
|
||||
}
|
||||
}
|
||||
|
||||
if (item.id === 'signal.threshold_result.cardinality') {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: ALERTS_HEADERS_THRESHOLD_CARDINALITY,
|
||||
description: {
|
||||
...description,
|
||||
values: [`count(${parsedValue.field}) == ${parsedValue.value}`],
|
||||
},
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: item.label ?? item.id,
|
||||
description,
|
||||
},
|
||||
];
|
||||
}, [])
|
||||
: [];
|
||||
};
|
||||
}: AlertSummaryRow['description']) => (
|
||||
<>
|
||||
<FieldValueCell
|
||||
contextId={timelineId}
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
linkValue={linkValue}
|
||||
isDraggable={isDraggable}
|
||||
values={values}
|
||||
/>
|
||||
<ActionCell
|
||||
contextId={timelineId}
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
fieldFromBrowserField={fieldFromBrowserField}
|
||||
linkValue={linkValue}
|
||||
timelineId={timelineId}
|
||||
values={values}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const summaryColumns: Array<EuiBasicTableColumn<SummaryRow>> = getSummaryColumns(getDescription);
|
||||
|
||||
|
@ -318,33 +64,10 @@ const AlertSummaryViewComponent: React.FC<{
|
|||
[browserFields, data, eventId, isDraggable, timelineId]
|
||||
);
|
||||
|
||||
const ruleId = useMemo(() => {
|
||||
const item = data.find((d) => d.field === 'signal.rule.id');
|
||||
return Array.isArray(item?.originalValue)
|
||||
? item?.originalValue[0]
|
||||
: item?.originalValue ?? null;
|
||||
}, [data]);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<SummaryView summaryColumns={summaryColumns} summaryRows={summaryRows} title={title} />
|
||||
{maybeRule?.note && (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<EuiTitle size="xxxs" data-test-subj="summary-view-guide">
|
||||
<h5>{i18n.INVESTIGATION_GUIDE}</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<Indent>
|
||||
<EuiText size="xs">
|
||||
<LineClamp lineClampHeight={4.5}>
|
||||
<MarkdownRenderer>{maybeRule.note}</MarkdownRenderer>
|
||||
</LineClamp>
|
||||
</EuiText>
|
||||
</Indent>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,8 +13,8 @@ import { isInvestigationTimeEnrichment } from './helpers';
|
|||
|
||||
export const getTooltipTitle = (type: string | undefined) =>
|
||||
isInvestigationTimeEnrichment(type)
|
||||
? i18n.INVESTIGATION_TOOLTIP_TITLE
|
||||
: i18n.INDICATOR_TOOLTIP_TITLE;
|
||||
? i18n.INVESTIGATION_ENRICHMENT_TITLE
|
||||
: i18n.INDICATOR_ENRICHMENT_TITLE;
|
||||
|
||||
export const getTooltipContent = (type: string | undefined) =>
|
||||
isInvestigationTimeEnrichment(type)
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { get } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { partition } from 'lodash';
|
||||
import * as i18n from './translations';
|
||||
import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti';
|
||||
import { getEnrichmentIdentifiers, isInvestigationTimeEnrichment } from './helpers';
|
||||
|
||||
import { FieldsData } from '../types';
|
||||
import { ActionCell } from '../table/action_cell';
|
||||
import { BrowserField, BrowserFields, TimelineEventsDetailsItem } from '../../../../../common';
|
||||
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view';
|
||||
|
||||
export interface ThreatSummaryDescription {
|
||||
browserField: BrowserField;
|
||||
data: FieldsData | undefined;
|
||||
eventId: string;
|
||||
index: number;
|
||||
provider: string | undefined;
|
||||
timelineId: string;
|
||||
value: string | undefined;
|
||||
isDraggable?: boolean;
|
||||
}
|
||||
|
||||
const EnrichmentFieldProvider = styled.span`
|
||||
margin-left: ${({ theme }) => theme.eui.paddingSizes.xs};
|
||||
white-space: nowrap;
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
const EnrichmentDescription: React.FC<ThreatSummaryDescription> = ({
|
||||
browserField,
|
||||
data,
|
||||
eventId,
|
||||
index,
|
||||
provider,
|
||||
timelineId,
|
||||
value,
|
||||
isDraggable,
|
||||
}) => {
|
||||
if (!data || !value) return null;
|
||||
const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${data.field}-${value}-${index}-${provider}`;
|
||||
return (
|
||||
<EuiFlexGroup key={key} direction="row" gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="eui-textBreakAll">
|
||||
<FormattedFieldValue
|
||||
contextId={timelineId}
|
||||
eventId={key}
|
||||
fieldFormat={data.format}
|
||||
fieldName={data.field}
|
||||
fieldType={data.type}
|
||||
isDraggable={isDraggable}
|
||||
isObjectArray={data.isObjectArray}
|
||||
value={value}
|
||||
/>
|
||||
{provider && (
|
||||
<EnrichmentFieldProvider>
|
||||
{i18n.PROVIDER_PREPOSITION} {provider}
|
||||
</EnrichmentFieldProvider>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{value && (
|
||||
<ActionCell
|
||||
data={data}
|
||||
contextId={timelineId}
|
||||
eventId={key}
|
||||
fieldFromBrowserField={browserField}
|
||||
timelineId={timelineId}
|
||||
values={[value]}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const EnrichmentSummaryComponent: React.FC<{
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineEventsDetailsItem[];
|
||||
enrichments: CtiEnrichment[];
|
||||
timelineId: string;
|
||||
eventId: string;
|
||||
isDraggable?: boolean;
|
||||
}> = ({ browserFields, data, enrichments, timelineId, eventId, isDraggable }) => {
|
||||
const parsedEnrichments = enrichments.map((enrichment, index) => {
|
||||
const { field, type, provider, value } = getEnrichmentIdentifiers(enrichment);
|
||||
const eventData = data.find((item) => item.field === field);
|
||||
const category = eventData?.category ?? '';
|
||||
const browserField = get([category, 'fields', field ?? ''], browserFields);
|
||||
|
||||
const fieldsData: FieldsData = {
|
||||
field: field ?? '',
|
||||
format: browserField?.format ?? '',
|
||||
type: browserField?.type ?? '',
|
||||
isObjectArray: eventData?.isObjectArray ?? false,
|
||||
};
|
||||
|
||||
return {
|
||||
fieldsData,
|
||||
type,
|
||||
provider,
|
||||
index,
|
||||
field,
|
||||
browserField,
|
||||
value,
|
||||
};
|
||||
});
|
||||
|
||||
const [investigation, indicator] = partition(parsedEnrichments, ({ type }) =>
|
||||
isInvestigationTimeEnrichment(type)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{indicator.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasBorder paddingSize="s" grow={false}>
|
||||
<ThreatSummaryPanelHeader
|
||||
title={i18n.INDICATOR_ENRICHMENT_TITLE}
|
||||
toolTipContent={i18n.INDICATOR_TOOLTIP_CONTENT}
|
||||
/>
|
||||
|
||||
{indicator.map(({ fieldsData, index, field, provider, browserField, value }) => (
|
||||
<EnrichedDataRow
|
||||
key={field}
|
||||
field={field}
|
||||
value={
|
||||
<EnrichmentDescription
|
||||
eventId={eventId}
|
||||
index={index}
|
||||
provider={provider}
|
||||
timelineId={timelineId}
|
||||
value={value}
|
||||
data={fieldsData}
|
||||
browserField={browserField}
|
||||
isDraggable={isDraggable}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{investigation.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasBorder paddingSize="s" grow={false}>
|
||||
<ThreatSummaryPanelHeader
|
||||
title={i18n.INVESTIGATION_ENRICHMENT_TITLE}
|
||||
toolTipContent={i18n.INVESTIGATION_TOOLTIP_CONTENT}
|
||||
/>
|
||||
|
||||
{investigation.map(({ fieldsData, index, field, provider, browserField, value }) => (
|
||||
<EnrichedDataRow
|
||||
key={field}
|
||||
field={field}
|
||||
value={
|
||||
<EnrichmentDescription
|
||||
eventId={eventId}
|
||||
index={index}
|
||||
provider={provider}
|
||||
timelineId={timelineId}
|
||||
value={value}
|
||||
data={fieldsData}
|
||||
browserField={browserField}
|
||||
isDraggable={isDraggable}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const EnrichmentSummary = React.memo(EnrichmentSummaryComponent);
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { NO_HOST_RISK_DATA_DESCRIPTION } from './translations';
|
||||
import { HostRiskSummary } from './host_risk_summary';
|
||||
|
||||
describe('HostRiskSummary', () => {
|
||||
it('renders host risk data', () => {
|
||||
const riskKeyword = 'test risk';
|
||||
const hostRisk = {
|
||||
loading: false,
|
||||
isModuleEnabled: true,
|
||||
result: [
|
||||
{
|
||||
host: {
|
||||
name: 'test-host-name',
|
||||
},
|
||||
risk_score: 9999,
|
||||
risk: riskKeyword,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<HostRiskSummary hostRisk={hostRisk} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText(riskKeyword)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders spinner when loading', () => {
|
||||
const hostRisk = {
|
||||
loading: true,
|
||||
isModuleEnabled: true,
|
||||
result: [],
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HostRiskSummary hostRisk={hostRisk} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('loading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no host data message when module is diabled', () => {
|
||||
const hostRisk = {
|
||||
loading: false,
|
||||
isModuleEnabled: false,
|
||||
result: [
|
||||
{
|
||||
host: {
|
||||
name: 'test-host-name',
|
||||
},
|
||||
risk_score: 9999,
|
||||
risk: 'test-risk',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<HostRiskSummary hostRisk={hostRisk} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText(NO_HOST_RISK_DATA_DESCRIPTION)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no host data message when there is no host data', () => {
|
||||
const hostRisk = {
|
||||
loading: false,
|
||||
isModuleEnabled: true,
|
||||
result: [],
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<HostRiskSummary hostRisk={hostRisk} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText(NO_HOST_RISK_DATA_DESCRIPTION)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import * as i18n from './translations';
|
||||
import { RISKY_HOSTS_DOC_LINK } from '../../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module';
|
||||
import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view';
|
||||
|
||||
const HostRiskSummaryComponent: React.FC<{
|
||||
hostRisk: HostRisk;
|
||||
}> = ({ hostRisk }) => (
|
||||
<>
|
||||
<EuiPanel hasBorder paddingSize="s" grow={false}>
|
||||
<ThreatSummaryPanelHeader
|
||||
title={i18n.HOST_RISK_DATA_TITLE}
|
||||
toolTipContent={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.alertDetails.overview.hostDataTooltipContent"
|
||||
defaultMessage="Risk classification is displayed only when available for a host. Ensure {hostsRiskScoreDocumentationLink} is enabled within your environment."
|
||||
values={{
|
||||
hostsRiskScoreDocumentationLink: (
|
||||
<EuiLink href={RISKY_HOSTS_DOC_LINK} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.alertDetails.overview.hostsRiskScoreLink"
|
||||
defaultMessage="Host Risk Score"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{hostRisk.loading && <EuiLoadingSpinner data-test-subj="loading" />}
|
||||
|
||||
{!hostRisk.loading && (!hostRisk.isModuleEnabled || hostRisk.result?.length === 0) && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued" size="xs">
|
||||
{i18n.NO_HOST_RISK_DATA_DESCRIPTION}
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hostRisk.isModuleEnabled && hostRisk.result && hostRisk.result.length > 0 && (
|
||||
<>
|
||||
<EnrichedDataRow field={'host.risk.keyword'} value={hostRisk.result[0].risk} />
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
|
||||
export const HostRiskSummary = React.memo(HostRiskSummaryComponent);
|
|
@ -30,8 +30,8 @@ const EnrichmentSectionHeader: React.FC<{ type?: ENRICHMENT_TYPES }> = ({ type }
|
|||
<EuiTitle size="xxxs">
|
||||
<h5>
|
||||
{type === ENRICHMENT_TYPES.IndicatorMatchRule
|
||||
? i18n.INDICATOR_TOOLTIP_TITLE
|
||||
: i18n.INVESTIGATION_TOOLTIP_TITLE}
|
||||
? i18n.INDICATOR_ENRICHMENT_TITLE
|
||||
: i18n.INVESTIGATION_ENRICHMENT_TITLE}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -9,28 +9,36 @@ import React from 'react';
|
|||
|
||||
import { ThreatSummaryView } from './threat_summary_view';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { useMountAppended } from '../../../utils/use_mount_appended';
|
||||
import { render } from '@testing-library/react';
|
||||
import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock';
|
||||
import { mockAlertDetailsData } from '../__mocks__';
|
||||
import { TimelineEventsDetailsItem } from '../../../../../../timelines/common';
|
||||
import { mockBrowserFields } from '../../../containers/source/mock';
|
||||
import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
timelines: { ...mockTimelines },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../table/action_cell');
|
||||
jest.mock('../table/field_name_cell');
|
||||
|
||||
describe('ThreatSummaryView', () => {
|
||||
const mount = useMountAppended();
|
||||
const eventId = '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31';
|
||||
const timelineId = 'detections-page';
|
||||
const data = mockAlertDetailsData as TimelineEventsDetailsItem[];
|
||||
const browserFields = mockBrowserFields;
|
||||
|
||||
it('renders a row for each enrichment', () => {
|
||||
it("renders 'Enriched with Threat Intelligence' panel with fields", () => {
|
||||
const enrichments = [
|
||||
buildEventEnrichmentMock({ 'matched.id': ['test.id'], 'matched.field': ['test.field'] }),
|
||||
buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }),
|
||||
];
|
||||
const wrapper = mount(
|
||||
const { getByText, getAllByTestId } = render(
|
||||
<TestProviders>
|
||||
<ThreatSummaryView
|
||||
data={data}
|
||||
|
@ -38,12 +46,13 @@ describe('ThreatSummaryView', () => {
|
|||
enrichments={enrichments}
|
||||
eventId={eventId}
|
||||
timelineId={timelineId}
|
||||
hostRisk={null}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="threat-summary-view"] .euiTableRow')).toHaveLength(
|
||||
enrichments.length
|
||||
);
|
||||
expect(getByText('Enriched with Threat Intelligence')).toBeInTheDocument();
|
||||
|
||||
expect(getAllByTestId('EnrichedDataRow')).toHaveLength(enrichments.length);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,172 +6,169 @@
|
|||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { get } from 'lodash/fp';
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiBasicTableColumn, EuiText, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
EuiPopoverTitle,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import { Indent, StyledEuiInMemoryTable } from '../summary_view';
|
||||
import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti';
|
||||
import { getEnrichmentIdentifiers } from './helpers';
|
||||
import { EnrichmentIcon } from './enrichment_icon';
|
||||
import { FieldsData } from '../types';
|
||||
import { ActionCell } from '../table/action_cell';
|
||||
import { BrowserField, BrowserFields, TimelineEventsDetailsItem } from '../../../../../common';
|
||||
import { FieldValueCell } from '../table/field_value_cell';
|
||||
|
||||
export interface ThreatSummaryItem {
|
||||
title: {
|
||||
title: string | undefined;
|
||||
type: string | undefined;
|
||||
};
|
||||
description: {
|
||||
browserField: BrowserField;
|
||||
data: FieldsData | undefined;
|
||||
eventId: string;
|
||||
index: number;
|
||||
provider: string | undefined;
|
||||
timelineId: string;
|
||||
value: string | undefined;
|
||||
};
|
||||
import { FieldsData } from '../types';
|
||||
|
||||
import { BrowserField, BrowserFields, TimelineEventsDetailsItem } from '../../../../../common';
|
||||
import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
import { HostRiskSummary } from './host_risk_summary';
|
||||
import { EnrichmentSummary } from './enrichment_summary';
|
||||
|
||||
export interface ThreatSummaryDescription {
|
||||
browserField: BrowserField;
|
||||
data: FieldsData | undefined;
|
||||
eventId: string;
|
||||
index: number;
|
||||
provider: string | undefined;
|
||||
timelineId: string;
|
||||
value: string | undefined;
|
||||
isDraggable?: boolean;
|
||||
}
|
||||
|
||||
const RightMargin = styled.span`
|
||||
margin-right: ${({ theme }) => theme.eui.paddingSizes.xs};
|
||||
min-width: 30px;
|
||||
const UppercaseEuiTitle = styled(EuiTitle)`
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const EnrichmentTitle: React.FC<ThreatSummaryItem['title']> = ({ title, type }) => (
|
||||
<>
|
||||
<RightMargin>
|
||||
<EuiTitle size="xxxs">
|
||||
<h5>{title}</h5>
|
||||
</EuiTitle>
|
||||
</RightMargin>
|
||||
<EnrichmentIcon type={type} />
|
||||
</>
|
||||
const ThreatSummaryPanelTitle: React.FC = ({ children }) => (
|
||||
<UppercaseEuiTitle size="xxxs">
|
||||
<h5>{children}</h5>
|
||||
</UppercaseEuiTitle>
|
||||
);
|
||||
|
||||
const EnrichmentDescription: React.FC<ThreatSummaryItem['description']> = ({
|
||||
browserField,
|
||||
data,
|
||||
eventId,
|
||||
index,
|
||||
provider,
|
||||
timelineId,
|
||||
const StyledEnrichmentFieldTitle = styled(EuiTitle)`
|
||||
width: 220px;
|
||||
`;
|
||||
|
||||
const EnrichmentFieldTitle: React.FC<{
|
||||
title: string | undefined;
|
||||
}> = ({ title }) => (
|
||||
<StyledEnrichmentFieldTitle size="xxxs">
|
||||
<h6>{title}</h6>
|
||||
</StyledEnrichmentFieldTitle>
|
||||
);
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
export const EnrichedDataRow: React.FC<{ field: string | undefined; value: React.ReactNode }> = ({
|
||||
field,
|
||||
value,
|
||||
}) => {
|
||||
if (!data || !value) return null;
|
||||
const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${data.field}-${value}-${index}-${provider}`;
|
||||
}) => (
|
||||
<StyledEuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="none"
|
||||
responsive
|
||||
alignItems="center"
|
||||
data-test-subj="EnrichedDataRow"
|
||||
>
|
||||
<EuiFlexItem style={{ flexShrink: 0 }} grow={false}>
|
||||
<EnrichmentFieldTitle title={field} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{value}</EuiFlexItem>
|
||||
</StyledEuiFlexGroup>
|
||||
);
|
||||
|
||||
export const ThreatSummaryPanelHeader: React.FC<{
|
||||
title: string;
|
||||
toolTipContent: React.ReactNode;
|
||||
}> = ({ title, toolTipContent }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}, [isPopoverOpen, setIsPopoverOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, [setIsPopoverOpen]);
|
||||
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<RightMargin>
|
||||
<FieldValueCell
|
||||
contextId={timelineId}
|
||||
data={data}
|
||||
eventId={key}
|
||||
fieldFromBrowserField={browserField}
|
||||
values={[value]}
|
||||
/>
|
||||
</RightMargin>
|
||||
{provider && (
|
||||
<>
|
||||
<RightMargin>
|
||||
<EuiText size="xs">
|
||||
<em>{i18n.PROVIDER_PREPOSITION}</em>
|
||||
</EuiText>
|
||||
</RightMargin>
|
||||
<RightMargin>
|
||||
<EuiText grow={false} size="xs">
|
||||
{provider}
|
||||
</EuiText>
|
||||
</RightMargin>
|
||||
</>
|
||||
)}
|
||||
{value && (
|
||||
<ActionCell
|
||||
data={data}
|
||||
contextId={timelineId}
|
||||
eventId={key}
|
||||
fieldFromBrowserField={browserField}
|
||||
timelineId={timelineId}
|
||||
values={[value]}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
<EuiFlexGroup direction="row" gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<ThreatSummaryPanelTitle>{title}</ThreatSummaryPanelTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
size="xs"
|
||||
iconSize="m"
|
||||
iconType="iInCircle"
|
||||
aria-label={i18n.INFORMATION_ARIA_LABEL}
|
||||
onClick={onClick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiPopoverTitle>{title}</EuiPopoverTitle>
|
||||
<EuiText size="s" style={{ width: '270px' }}>
|
||||
{toolTipContent}
|
||||
</EuiText>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const buildThreatSummaryItems = (
|
||||
browserFields: BrowserFields,
|
||||
data: TimelineEventsDetailsItem[],
|
||||
enrichments: CtiEnrichment[],
|
||||
timelineId: string,
|
||||
eventId: string
|
||||
) => {
|
||||
return enrichments.map((enrichment, index) => {
|
||||
const { field, type, value, provider } = getEnrichmentIdentifiers(enrichment);
|
||||
const eventData = data.find((item) => item.field === field);
|
||||
const category = eventData?.category ?? '';
|
||||
const browserField = get([category, 'fields', field ?? ''], browserFields);
|
||||
|
||||
const fieldsData = {
|
||||
field,
|
||||
format: browserField?.format ?? '',
|
||||
type: browserField?.type ?? '',
|
||||
isObjectArray: eventData?.isObjectArray,
|
||||
};
|
||||
|
||||
return {
|
||||
title: {
|
||||
title: field,
|
||||
type,
|
||||
},
|
||||
description: {
|
||||
eventId,
|
||||
index,
|
||||
provider,
|
||||
timelineId,
|
||||
value,
|
||||
data: fieldsData,
|
||||
browserField,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ThreatSummaryItem>> = [
|
||||
{
|
||||
field: 'title',
|
||||
truncateText: false,
|
||||
render: EnrichmentTitle,
|
||||
width: '220px',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
className: 'flyoutOverviewDescription',
|
||||
field: 'description',
|
||||
truncateText: false,
|
||||
render: EnrichmentDescription,
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
|
||||
const ThreatSummaryViewComponent: React.FC<{
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineEventsDetailsItem[];
|
||||
enrichments: CtiEnrichment[];
|
||||
eventId: string;
|
||||
timelineId: string;
|
||||
}> = ({ browserFields, data, enrichments, eventId, timelineId }) => (
|
||||
<Indent>
|
||||
<StyledEuiInMemoryTable
|
||||
columns={columns}
|
||||
compressed
|
||||
data-test-subj="threat-summary-view"
|
||||
items={buildThreatSummaryItems(browserFields, data, enrichments, timelineId, eventId)}
|
||||
/>
|
||||
</Indent>
|
||||
);
|
||||
hostRisk: HostRisk | null;
|
||||
isDraggable?: boolean;
|
||||
}> = ({ browserFields, data, enrichments, eventId, timelineId, hostRisk, isDraggable }) => {
|
||||
if (!hostRisk && enrichments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<EuiTitle size="xxxs">
|
||||
<h5>{i18n.ENRICHED_DATA}</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup direction="column" gutterSize="m" style={{ flexGrow: 0 }}>
|
||||
{hostRisk && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostRiskSummary hostRisk={hostRisk} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EnrichmentSummary
|
||||
browserFields={browserFields}
|
||||
data={data}
|
||||
enrichments={enrichments}
|
||||
timelineId={timelineId}
|
||||
eventId={eventId}
|
||||
isDraggable={isDraggable}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent);
|
||||
|
|
|
@ -14,20 +14,27 @@ export const PROVIDER_PREPOSITION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const INDICATOR_TOOLTIP_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle',
|
||||
export const INDICATOR_ENRICHMENT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTitle',
|
||||
{
|
||||
defaultMessage: 'Threat Match Detected',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVESTIGATION_TOOLTIP_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle',
|
||||
export const INVESTIGATION_ENRICHMENT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle',
|
||||
{
|
||||
defaultMessage: 'Enriched with Threat Intelligence',
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_RISK_DATA_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.alertDetails.overview.hostRiskDataTitle',
|
||||
{
|
||||
defaultMessage: 'Host Risk Data',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDICATOR_TOOLTIP_CONTENT = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent',
|
||||
{
|
||||
|
@ -36,6 +43,13 @@ export const INDICATOR_TOOLTIP_CONTENT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const INFORMATION_ARIA_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.informationAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Information',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVESTIGATION_TOOLTIP_CONTENT = i18n.translate(
|
||||
'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent',
|
||||
{
|
||||
|
@ -82,6 +96,13 @@ export const NO_ENRICHMENTS_FOUND_DESCRIPTION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const NO_HOST_RISK_DATA_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.alertDetails.noRiskDataDescription',
|
||||
{
|
||||
defaultMessage: 'These is no host risk data found for this alert',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', {
|
||||
defaultMessage: 'please check out our documentation',
|
||||
});
|
||||
|
@ -117,3 +138,10 @@ export const ENRICHMENT_LOOKBACK_END_DATE = i18n.translate(
|
|||
export const REFRESH = i18n.translate('xpack.securitySolution.alertDetails.refresh', {
|
||||
defaultMessage: 'Refresh',
|
||||
});
|
||||
|
||||
export const ENRICHED_DATA = i18n.translate(
|
||||
'xpack.securitySolution.alertDetails.overview.enrichedDataTitle',
|
||||
{
|
||||
defaultMessage: 'Enriched data',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -24,6 +24,16 @@ import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enric
|
|||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../containers/cti/event_enrichment');
|
||||
|
||||
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_with_fallback', () => {
|
||||
return {
|
||||
useRuleWithFallback: jest.fn().mockReturnValue({
|
||||
rule: {
|
||||
note: 'investigation guide',
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../link_to');
|
||||
describe('EventDetails', () => {
|
||||
const mount = useMountAppended();
|
||||
|
@ -37,6 +47,7 @@ describe('EventDetails', () => {
|
|||
timelineTabType: TimelineTabs.query,
|
||||
timelineId: 'test',
|
||||
eventView: EventsViewType.summaryView,
|
||||
hostRisk: { fields: [], loading: true },
|
||||
};
|
||||
|
||||
const alertsProps = {
|
||||
|
@ -115,6 +126,12 @@ describe('EventDetails', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('summary view tab', () => {
|
||||
it('render investigation guide', () => {
|
||||
expect(alertsWrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('threat intel tab', () => {
|
||||
it('renders a "no enrichments" panel view if there are no enrichments', () => {
|
||||
alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click');
|
||||
|
|
|
@ -10,10 +10,10 @@ import {
|
|||
EuiTabbedContentTab,
|
||||
EuiSpacer,
|
||||
EuiLoadingContent,
|
||||
EuiLoadingSpinner,
|
||||
EuiNotificationBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -38,6 +38,9 @@ import {
|
|||
import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker';
|
||||
import { Reason } from './reason';
|
||||
|
||||
import { InvestigationGuideView } from './investigation_guide_view';
|
||||
import { HostRisk } from '../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
|
||||
type EventViewTab = EuiTabbedContentTab;
|
||||
|
||||
export type EventViewId =
|
||||
|
@ -60,8 +63,14 @@ interface Props {
|
|||
isDraggable?: boolean;
|
||||
timelineTabType: TimelineTabs | 'flyout';
|
||||
timelineId: string;
|
||||
hostRisk: HostRisk | null;
|
||||
}
|
||||
|
||||
export const Indent = styled.div`
|
||||
padding: 0 8px;
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
const StyledEuiTabbedContent = styled(EuiTabbedContent)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -99,6 +108,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
isDraggable,
|
||||
timelineId,
|
||||
timelineTabType,
|
||||
hostRisk,
|
||||
}) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState<EventViewId>(EventsViewType.summaryView);
|
||||
const handleTabClick = useCallback(
|
||||
|
@ -151,8 +161,11 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
title: i18n.DUCOMENT_SUMMARY,
|
||||
}}
|
||||
/>
|
||||
{enrichmentCount > 0 && (
|
||||
|
||||
{(enrichmentCount > 0 || hostRisk) && (
|
||||
<ThreatSummaryView
|
||||
isDraggable={isDraggable}
|
||||
hostRisk={hostRisk}
|
||||
browserFields={browserFields}
|
||||
data={data}
|
||||
eventId={id}
|
||||
|
@ -160,11 +173,14 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
enrichments={allEnrichments}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEnrichmentsLoading && (
|
||||
<>
|
||||
<EuiLoadingContent lines={2} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<InvestigationGuideView data={data} />
|
||||
</>
|
||||
),
|
||||
}
|
||||
|
@ -179,6 +195,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
enrichmentCount,
|
||||
allEnrichments,
|
||||
isEnrichmentsLoading,
|
||||
hostRisk,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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 { get, getOr, find, isEmpty } from 'lodash/fp';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import {
|
||||
ALERTS_HEADERS_RISK_SCORE,
|
||||
ALERTS_HEADERS_RULE,
|
||||
ALERTS_HEADERS_SEVERITY,
|
||||
ALERTS_HEADERS_THRESHOLD_CARDINALITY,
|
||||
ALERTS_HEADERS_THRESHOLD_COUNT,
|
||||
ALERTS_HEADERS_THRESHOLD_TERMS,
|
||||
ALERTS_HEADERS_RULE_NAME,
|
||||
SIGNAL_STATUS,
|
||||
ALERTS_HEADERS_TARGET_IMPORT_HASH,
|
||||
TIMESTAMP,
|
||||
ALERTS_HEADERS_RULE_DESCRIPTION,
|
||||
} from '../../../detections/components/alerts_table/translations';
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
IP_FIELD_TYPE,
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip';
|
||||
import { SummaryRow } from './helpers';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
|
||||
import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check';
|
||||
import { EventCode } from '../../../../common/ecs/event';
|
||||
|
||||
interface EventSummaryField {
|
||||
id: string;
|
||||
label?: string;
|
||||
linkField?: string;
|
||||
fieldType?: string;
|
||||
overrideField?: string;
|
||||
}
|
||||
|
||||
const defaultDisplayFields: EventSummaryField[] = [
|
||||
{ id: 'signal.status', label: SIGNAL_STATUS },
|
||||
{ id: '@timestamp', label: TIMESTAMP },
|
||||
{
|
||||
id: SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
linkField: 'signal.rule.id',
|
||||
label: ALERTS_HEADERS_RULE,
|
||||
},
|
||||
{ id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY },
|
||||
{ id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE },
|
||||
{ id: 'host.name' },
|
||||
{ id: 'agent.id', overrideField: AGENT_STATUS_FIELD_NAME, label: i18n.AGENT_STATUS },
|
||||
{ id: 'user.name' },
|
||||
{ id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
|
||||
{ id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
|
||||
{ id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT },
|
||||
{ id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS },
|
||||
{ id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY },
|
||||
];
|
||||
|
||||
const processCategoryFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'process.name' },
|
||||
{ id: 'process.parent.name' },
|
||||
{ id: 'process.args' },
|
||||
];
|
||||
|
||||
const networkCategoryFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'destination.address' },
|
||||
{ id: 'destination.port' },
|
||||
{ id: 'process.name' },
|
||||
];
|
||||
|
||||
const memoryShellCodeAlertFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME },
|
||||
{
|
||||
id: 'Target.process.thread.Ext.start_address_details.memory_pe.imphash',
|
||||
label: ALERTS_HEADERS_TARGET_IMPORT_HASH,
|
||||
},
|
||||
];
|
||||
|
||||
const behaviorAlertFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'rule.description', label: ALERTS_HEADERS_RULE_DESCRIPTION },
|
||||
];
|
||||
|
||||
const memorySignatureAlertFields: EventSummaryField[] = [
|
||||
...defaultDisplayFields,
|
||||
{ id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME },
|
||||
];
|
||||
|
||||
function getEventFieldsToDisplay({
|
||||
eventCategory,
|
||||
eventCode,
|
||||
}: {
|
||||
eventCategory: string;
|
||||
eventCode?: string;
|
||||
}): EventSummaryField[] {
|
||||
switch (eventCode) {
|
||||
// memory protection fields
|
||||
case EventCode.SHELLCODE_THREAD:
|
||||
return memoryShellCodeAlertFields;
|
||||
case EventCode.MEMORY_SIGNATURE:
|
||||
return memorySignatureAlertFields;
|
||||
case EventCode.BEHAVIOR:
|
||||
return behaviorAlertFields;
|
||||
}
|
||||
|
||||
switch (eventCategory) {
|
||||
case 'network':
|
||||
return networkCategoryFields;
|
||||
|
||||
case 'process':
|
||||
return processCategoryFields;
|
||||
}
|
||||
|
||||
return defaultDisplayFields;
|
||||
}
|
||||
|
||||
export const getSummaryRows = ({
|
||||
data,
|
||||
browserFields,
|
||||
timelineId,
|
||||
eventId,
|
||||
isDraggable = false,
|
||||
}: {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
browserFields: BrowserFields;
|
||||
timelineId: string;
|
||||
eventId: string;
|
||||
isDraggable?: boolean;
|
||||
}) => {
|
||||
const eventCategoryField = find({ category: 'event', field: 'event.category' }, data);
|
||||
|
||||
const eventCategory = Array.isArray(eventCategoryField?.originalValue)
|
||||
? eventCategoryField?.originalValue[0]
|
||||
: eventCategoryField?.originalValue;
|
||||
|
||||
const eventCodeField = find({ category: 'event', field: 'event.code' }, data);
|
||||
|
||||
const eventCode = Array.isArray(eventCodeField?.originalValue)
|
||||
? eventCodeField?.originalValue?.[0]
|
||||
: eventCodeField?.originalValue;
|
||||
|
||||
const tableFields = getEventFieldsToDisplay({ eventCategory, eventCode });
|
||||
|
||||
return data != null
|
||||
? tableFields.reduce<SummaryRow[]>((acc, item) => {
|
||||
const initialDescription = {
|
||||
contextId: timelineId,
|
||||
eventId,
|
||||
isDraggable,
|
||||
value: null,
|
||||
fieldType: 'string',
|
||||
linkValue: undefined,
|
||||
timelineId,
|
||||
};
|
||||
const field = data.find((d) => d.field === item.id);
|
||||
if (!field || isEmpty(field?.values)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const linkValueField =
|
||||
item.linkField != null && data.find((d) => d.field === item.linkField);
|
||||
const linkValue = getOr(null, 'originalValue.0', linkValueField);
|
||||
const value = getOr(null, 'originalValue.0', field);
|
||||
const category = field.category ?? '';
|
||||
const fieldName = field.field ?? '';
|
||||
|
||||
const browserField = get([category, 'fields', fieldName], browserFields);
|
||||
const description = {
|
||||
...initialDescription,
|
||||
data: {
|
||||
field: field.field,
|
||||
format: browserField?.format ?? '',
|
||||
type: browserField?.type ?? '',
|
||||
isObjectArray: field.isObjectArray,
|
||||
...(item.overrideField ? { field: item.overrideField } : {}),
|
||||
},
|
||||
values: field.values,
|
||||
linkValue: linkValue ?? undefined,
|
||||
fieldFromBrowserField: browserField,
|
||||
};
|
||||
|
||||
if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (item.id === 'signal.threshold_result.terms') {
|
||||
try {
|
||||
const terms = getOr(null, 'originalValue', field);
|
||||
const parsedValue = terms.map((term: string) => JSON.parse(term));
|
||||
const thresholdTerms = (parsedValue ?? []).map(
|
||||
(entry: { field: string; value: string }) => {
|
||||
return {
|
||||
title: `${entry.field} [threshold]`,
|
||||
description: {
|
||||
...description,
|
||||
values: [entry.value],
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
return [...acc, ...thresholdTerms];
|
||||
} catch (err) {
|
||||
return [...acc];
|
||||
}
|
||||
}
|
||||
|
||||
if (item.id === 'signal.threshold_result.cardinality') {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: ALERTS_HEADERS_THRESHOLD_CARDINALITY,
|
||||
description: {
|
||||
...description,
|
||||
values: [`count(${parsedValue.field}) == ${parsedValue.value}`],
|
||||
},
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: item.label ?? item.id,
|
||||
description,
|
||||
},
|
||||
];
|
||||
}, [])
|
||||
: [];
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { EuiSpacer, EuiHorizontalRule, EuiTitle, EuiText } from '@elastic/eui';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback';
|
||||
import { MarkdownRenderer } from '../markdown_editor';
|
||||
import { LineClamp } from '../line_clamp';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common';
|
||||
|
||||
export const Indent = styled.div`
|
||||
padding: 0 8px;
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
const InvestigationGuideViewComponent: React.FC<{
|
||||
data: TimelineEventsDetailsItem[];
|
||||
}> = ({ data }) => {
|
||||
const ruleId = useMemo(() => {
|
||||
const item = data.find((d) => d.field === 'signal.rule.id');
|
||||
return Array.isArray(item?.originalValue)
|
||||
? item?.originalValue[0]
|
||||
: item?.originalValue ?? null;
|
||||
}, [data]);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
|
||||
if (!maybeRule?.note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<EuiTitle size="xxxs" data-test-subj="summary-view-guide">
|
||||
<h5>{i18n.INVESTIGATION_GUIDE}</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<Indent>
|
||||
<EuiText size="xs">
|
||||
<LineClamp lineClampHeight={4.5}>
|
||||
<MarkdownRenderer>{maybeRule.note}</MarkdownRenderer>
|
||||
</LineClamp>
|
||||
</EuiText>
|
||||
</Indent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const InvestigationGuideView = React.memo(InvestigationGuideViewComponent);
|
|
@ -36,7 +36,7 @@ export const FieldValueCell = React.memo(
|
|||
values,
|
||||
}: FieldValueCellProps) => {
|
||||
return (
|
||||
<div>
|
||||
<div data-test-subj={`event-field-${data.field}`}>
|
||||
{values != null &&
|
||||
values.map((value, i) => {
|
||||
if (fieldFromBrowserField == null) {
|
||||
|
|
|
@ -11,7 +11,6 @@ import { cloneDeep } from 'lodash/fp';
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { useRiskyHostLinks } from '../../containers/overview_risky_host_links/use_risky_host_links';
|
||||
import { mockTheme } from '../overview_cti_links/mock';
|
||||
import { RiskyHostLinks } from '.';
|
||||
import { createStore, State } from '../../../common/store';
|
||||
|
@ -23,11 +22,12 @@ import {
|
|||
} from '../../../common/mock';
|
||||
import { useRiskyHostsDashboardButtonHref } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href';
|
||||
import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links';
|
||||
import { useHostsRiskScore } from '../../containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
jest.mock('../../containers/overview_risky_host_links/use_risky_host_links');
|
||||
const useRiskyHostLinksMock = useRiskyHostLinks as jest.Mock;
|
||||
jest.mock('../../containers/overview_risky_host_links/use_hosts_risk_score');
|
||||
const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock;
|
||||
|
||||
jest.mock('../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href');
|
||||
const useRiskyHostsDashboardButtonHrefMock = useRiskyHostsDashboardButtonHref as jest.Mock;
|
||||
|
@ -51,10 +51,10 @@ describe('RiskyHostLinks', () => {
|
|||
});
|
||||
|
||||
it('renders enabled module view if module is enabled', () => {
|
||||
useRiskyHostLinksMock.mockReturnValueOnce({
|
||||
useHostsRiskScoreMock.mockReturnValueOnce({
|
||||
loading: false,
|
||||
isModuleEnabled: true,
|
||||
listItems: [],
|
||||
result: [],
|
||||
});
|
||||
|
||||
render(
|
||||
|
@ -62,10 +62,10 @@ describe('RiskyHostLinks', () => {
|
|||
<I18nProvider>
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<RiskyHostLinks
|
||||
to={'now'}
|
||||
from={'now-30d'}
|
||||
deleteQuery={jest.fn()}
|
||||
setQuery={jest.fn()}
|
||||
timerange={{
|
||||
to: 'now',
|
||||
from: 'now-30d',
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
|
@ -76,10 +76,10 @@ describe('RiskyHostLinks', () => {
|
|||
});
|
||||
|
||||
it('renders disabled module view if module is disabled', () => {
|
||||
useRiskyHostLinksMock.mockReturnValueOnce({
|
||||
useHostsRiskScoreMock.mockReturnValueOnce({
|
||||
loading: false,
|
||||
isModuleEnabled: false,
|
||||
listItems: [],
|
||||
result: [],
|
||||
});
|
||||
|
||||
render(
|
||||
|
@ -87,10 +87,10 @@ describe('RiskyHostLinks', () => {
|
|||
<I18nProvider>
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<RiskyHostLinks
|
||||
to={'now'}
|
||||
from={'now-30d'}
|
||||
deleteQuery={jest.fn()}
|
||||
setQuery={jest.fn()}
|
||||
timerange={{
|
||||
to: 'now',
|
||||
from: 'now-30d',
|
||||
}}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -7,18 +7,25 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
|
||||
import { useRiskyHostLinks } from '../../containers/overview_risky_host_links/use_risky_host_links';
|
||||
import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module';
|
||||
import { RiskyHostsDisabledModule } from './risky_hosts_disabled_module';
|
||||
export type RiskyHostLinksProps = Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery'>;
|
||||
import { useHostsRiskScore } from '../../containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
export interface RiskyHostLinksProps {
|
||||
timerange: { to: string; from: string };
|
||||
}
|
||||
|
||||
const RiskyHostLinksComponent: React.FC<RiskyHostLinksProps> = (props) => {
|
||||
const { listItems, isModuleEnabled } = useRiskyHostLinks(props);
|
||||
const RiskyHostLinksComponent: React.FC<RiskyHostLinksProps> = ({ timerange }) => {
|
||||
const hostRiskScore = useHostsRiskScore({ timerange });
|
||||
|
||||
switch (isModuleEnabled) {
|
||||
switch (hostRiskScore?.isModuleEnabled) {
|
||||
case true:
|
||||
return <RiskyHostsEnabledModule to={props.to} from={props.from} listItems={listItems} />;
|
||||
return (
|
||||
<RiskyHostsEnabledModule
|
||||
to={timerange.to}
|
||||
from={timerange.from}
|
||||
hostRiskScore={hostRiskScore}
|
||||
/>
|
||||
);
|
||||
case false:
|
||||
return <RiskyHostsDisabledModule />;
|
||||
case undefined:
|
||||
|
|
|
@ -12,7 +12,7 @@ import { DisabledLinkPanel } from '../link_panel/disabled_link_panel';
|
|||
import { RiskyHostsPanelView } from './risky_hosts_panel_view';
|
||||
import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module';
|
||||
|
||||
const RISKY_HOSTS_DOC_LINK =
|
||||
export const RISKY_HOSTS_DOC_LINK =
|
||||
'https://www.github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md';
|
||||
|
||||
export const RiskyHostsDisabledModuleComponent = () => (
|
||||
|
|
|
@ -52,7 +52,19 @@ describe('RiskyHostsEnabledModule', () => {
|
|||
<I18nProvider>
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<RiskyHostsEnabledModule
|
||||
listItems={[{ title: 'a', count: 1, path: '' }]}
|
||||
hostRiskScore={{
|
||||
loading: false,
|
||||
isModuleEnabled: true,
|
||||
result: [
|
||||
{
|
||||
host: {
|
||||
name: 'a',
|
||||
},
|
||||
risk_score: 1,
|
||||
risk: '',
|
||||
},
|
||||
],
|
||||
}}
|
||||
to={'now'}
|
||||
from={'now-30d'}
|
||||
/>
|
||||
|
|
|
@ -5,17 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { RiskyHostsPanelView } from './risky_hosts_panel_view';
|
||||
import { LinkPanelListItem } from '../link_panel';
|
||||
import { useRiskyHostsDashboardButtonHref } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href';
|
||||
import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links';
|
||||
import { HostRisk } from '../../containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
import { HostsRiskScore } from '../../../../common';
|
||||
|
||||
const getListItemsFromHits = (items: HostsRiskScore[]): LinkPanelListItem[] => {
|
||||
return items.map(({ host, risk_score: count, risk: copy }) => ({
|
||||
title: host.name,
|
||||
count,
|
||||
copy,
|
||||
path: '',
|
||||
}));
|
||||
};
|
||||
|
||||
const RiskyHostsEnabledModuleComponent: React.FC<{
|
||||
from: string;
|
||||
listItems: LinkPanelListItem[];
|
||||
hostRiskScore: HostRisk;
|
||||
to: string;
|
||||
}> = ({ listItems, to, from }) => {
|
||||
}> = ({ hostRiskScore, to, from }) => {
|
||||
const listItems = useMemo(
|
||||
() => getListItemsFromHits(hostRiskScore?.result || []),
|
||||
[hostRiskScore]
|
||||
);
|
||||
const { buttonHref } = useRiskyHostsDashboardButtonHref(to, from);
|
||||
const { listItemsWithLinks } = useRiskyHostsDashboardLinks(to, from, listItems);
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { LinkPanelViewProps } from '../link_panel/types';
|
|||
import { Link } from '../link_panel/link';
|
||||
import * as i18n from './translations';
|
||||
import { VIEW_DASHBOARD } from '../overview_cti_links/translations';
|
||||
import { QUERY_ID as RiskyHostsQueryId } from '../../containers/overview_risky_host_links/use_risky_host_links';
|
||||
import { QUERY_ID as RiskyHostsQueryId } from '../../containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
import { NavigateToHost } from './navigate_to_host';
|
||||
|
||||
const columns: Array<EuiTableFieldDataColumnType<LinkPanelListItem>> = [
|
||||
|
|
|
@ -9,61 +9,61 @@ import { i18n } from '@kbn/i18n';
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useRiskyHostsComplete } from './use_risky_hosts';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { inputsActions } from '../../../common/store/actions';
|
||||
import { LinkPanelListItem } from '../../components/link_panel';
|
||||
import { RISKY_HOSTS_INDEX } from '../../../../common/constants';
|
||||
|
||||
import { HOST_RISK_SCORES_INDEX } from '../../../../common/constants';
|
||||
import { isIndexNotFoundError } from '../../../common/utils/exceptions';
|
||||
import { HostsRiskScore } from '../../../../common';
|
||||
import { useHostsRiskScoreComplete } from './use_hosts_risk_score_complete';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
|
||||
export const QUERY_ID = 'risky_hosts';
|
||||
export const QUERY_ID = 'host_risk_score';
|
||||
const noop = () => {};
|
||||
|
||||
export interface RiskyHost {
|
||||
host: {
|
||||
name: string;
|
||||
};
|
||||
risk_score: number;
|
||||
risk: string;
|
||||
}
|
||||
|
||||
const isRecord = (item: unknown): item is Record<string, unknown> =>
|
||||
typeof item === 'object' && !!item;
|
||||
|
||||
const isRiskyHostHit = (item: unknown): item is RiskyHost =>
|
||||
const isHostsRiskScoreHit = (item: unknown): item is HostsRiskScore =>
|
||||
isRecord(item) &&
|
||||
isRecord(item.host) &&
|
||||
typeof item.host.name === 'string' &&
|
||||
typeof item.risk_score === 'number' &&
|
||||
typeof item.risk === 'string';
|
||||
|
||||
const getListItemsFromHits = (items: RiskyHost[]): LinkPanelListItem[] => {
|
||||
return items.map(({ host, risk_score: count, risk: copy }) => ({
|
||||
title: host.name,
|
||||
count,
|
||||
copy,
|
||||
path: '',
|
||||
}));
|
||||
};
|
||||
export interface HostRisk {
|
||||
loading: boolean;
|
||||
isModuleEnabled?: boolean;
|
||||
result?: HostsRiskScore[];
|
||||
}
|
||||
|
||||
export const useRiskyHostLinks = ({ to, from }: { to: string; from: string }) => {
|
||||
export const useHostsRiskScore = ({
|
||||
timerange,
|
||||
hostName,
|
||||
}: {
|
||||
timerange?: { to: string; from: string };
|
||||
hostName?: string;
|
||||
}): HostRisk | null => {
|
||||
const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled');
|
||||
const [isModuleEnabled, setIsModuleEnabled] = useState<boolean | undefined>(undefined);
|
||||
const [loading, setLoading] = useState<boolean>(riskyHostsFeatureEnabled);
|
||||
|
||||
const { addError } = useAppToasts();
|
||||
const { data } = useKibana().services;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { error, loading, result, start } = useRiskyHostsComplete();
|
||||
const { error, result, start, loading: isHostsRiskScoreLoading } = useHostsRiskScoreComplete();
|
||||
|
||||
const deleteQuery = useCallback(() => {
|
||||
dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: QUERY_ID }));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && result) {
|
||||
if (!isHostsRiskScoreLoading && result) {
|
||||
setIsModuleEnabled(true);
|
||||
setLoading(false);
|
||||
dispatch(
|
||||
inputsActions.setQuery({
|
||||
inputId: 'global',
|
||||
|
@ -72,43 +72,51 @@ export const useRiskyHostLinks = ({ to, from }: { to: string; from: string }) =>
|
|||
dsl: result.inspect?.dsl ?? [],
|
||||
response: [JSON.stringify(result.rawResponse, null, 2)],
|
||||
},
|
||||
loading,
|
||||
loading: isHostsRiskScoreLoading,
|
||||
refetch: noop,
|
||||
})
|
||||
);
|
||||
}
|
||||
return deleteQuery;
|
||||
}, [deleteQuery, dispatch, loading, result, setIsModuleEnabled]);
|
||||
}, [deleteQuery, dispatch, isHostsRiskScoreLoading, result, setIsModuleEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
if (isIndexNotFoundError(error)) {
|
||||
setIsModuleEnabled(false);
|
||||
setLoading(false);
|
||||
} else {
|
||||
addError(error, {
|
||||
title: i18n.translate('xpack.securitySolution.overview.riskyHostsError', {
|
||||
defaultMessage: 'Error Fetching Risky Hosts',
|
||||
title: i18n.translate('xpack.securitySolution.overview.hostsRiskError', {
|
||||
defaultMessage: 'Error Fetching Hosts Risk',
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
setIsModuleEnabled(true);
|
||||
}
|
||||
}
|
||||
}, [addError, error, setIsModuleEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
start({
|
||||
data,
|
||||
timerange: { to, from, interval: '' },
|
||||
defaultIndex: [RISKY_HOSTS_INDEX],
|
||||
filterQuery: '',
|
||||
});
|
||||
}, [start, data, to, from]);
|
||||
if (riskyHostsFeatureEnabled && (hostName || timerange)) {
|
||||
start({
|
||||
data,
|
||||
timerange: timerange ? { to: timerange.to, from: timerange.from, interval: '' } : undefined,
|
||||
hostName,
|
||||
defaultIndex: [HOST_RISK_SCORES_INDEX],
|
||||
});
|
||||
}
|
||||
}, [start, data, timerange, hostName, riskyHostsFeatureEnabled]);
|
||||
|
||||
if ((!hostName && !timerange) || !riskyHostsFeatureEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hits = result?.rawResponse?.hits?.hits;
|
||||
|
||||
return {
|
||||
listItems: isRiskyHostHit(result?.rawResponse?.hits?.hits?.[0]?._source)
|
||||
? getListItemsFromHits(
|
||||
result?.rawResponse?.hits?.hits?.map((hit) => hit._source) as RiskyHost[]
|
||||
)
|
||||
result: isHostsRiskScoreHit(hits?.[0]?._source)
|
||||
? (hits?.map((hit) => hit._source) as HostsRiskScore[])
|
||||
: [],
|
||||
isModuleEnabled,
|
||||
loading,
|
|
@ -15,28 +15,28 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/public';
|
||||
import {
|
||||
HostsQueries,
|
||||
HostsRiskyHostsRequestOptions,
|
||||
HostsRiskyHostsStrategyResponse,
|
||||
HostsRiskScoreRequestOptions,
|
||||
HostsRiskScoreStrategyResponse,
|
||||
} from '../../../../common';
|
||||
|
||||
type GetRiskyHostsProps = HostsRiskyHostsRequestOptions & {
|
||||
type GetHostsRiskScoreProps = HostsRiskScoreRequestOptions & {
|
||||
data: DataPublicPluginStart;
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
export const getRiskyHosts = ({
|
||||
export const getHostsRiskScore = ({
|
||||
data,
|
||||
defaultIndex,
|
||||
filterQuery,
|
||||
timerange,
|
||||
hostName,
|
||||
signal,
|
||||
}: GetRiskyHostsProps): Observable<HostsRiskyHostsStrategyResponse> =>
|
||||
data.search.search<HostsRiskyHostsRequestOptions, HostsRiskyHostsStrategyResponse>(
|
||||
}: GetHostsRiskScoreProps): Observable<HostsRiskScoreStrategyResponse> =>
|
||||
data.search.search<HostsRiskScoreRequestOptions, HostsRiskScoreStrategyResponse>(
|
||||
{
|
||||
defaultIndex,
|
||||
factoryQueryType: HostsQueries.riskyHosts,
|
||||
filterQuery,
|
||||
factoryQueryType: HostsQueries.hostsRiskScore,
|
||||
timerange,
|
||||
hostName,
|
||||
},
|
||||
{
|
||||
strategy: 'securitySolutionSearchStrategy',
|
||||
|
@ -44,16 +44,16 @@ export const getRiskyHosts = ({
|
|||
}
|
||||
);
|
||||
|
||||
export const getRiskyHostsComplete = (
|
||||
props: GetRiskyHostsProps
|
||||
): Observable<HostsRiskyHostsStrategyResponse> => {
|
||||
return getRiskyHosts(props).pipe(
|
||||
export const getHostsRiskScoreComplete = (
|
||||
props: GetHostsRiskScoreProps
|
||||
): Observable<HostsRiskScoreStrategyResponse> => {
|
||||
return getHostsRiskScore(props).pipe(
|
||||
filter((response) => {
|
||||
return isErrorResponse(response) || isCompleteResponse(response);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getRiskyHostsWithOptionalSignal = withOptionalSignal(getRiskyHostsComplete);
|
||||
const getHostsRiskScoreWithOptionalSignal = withOptionalSignal(getHostsRiskScoreComplete);
|
||||
|
||||
export const useRiskyHostsComplete = () => useObservable(getRiskyHostsWithOptionalSignal);
|
||||
export const useHostsRiskScoreComplete = () => useObservable(getHostsRiskScoreWithOptionalSignal);
|
|
@ -31,8 +31,8 @@ import {
|
|||
} from '../components/overview_cti_links/mock';
|
||||
import { useCtiDashboardLinks } from '../containers/overview_cti_links';
|
||||
import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges';
|
||||
import { useRiskyHostLinks } from '../containers/overview_risky_host_links/use_risky_host_links';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../common/containers/source');
|
||||
|
@ -86,9 +86,9 @@ jest.mock('../containers/overview_cti_links/use_is_threat_intel_module_enabled')
|
|||
const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock;
|
||||
useIsThreatIntelModuleEnabledMock.mockReturnValue(true);
|
||||
|
||||
jest.mock('../containers/overview_risky_host_links/use_risky_host_links');
|
||||
const useRiskyHostLinksMock = useRiskyHostLinks as jest.Mock;
|
||||
useRiskyHostLinksMock.mockReturnValue({
|
||||
jest.mock('../containers/overview_risky_host_links/use_hosts_risk_score');
|
||||
const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock;
|
||||
useHostsRiskScoreMock.mockReturnValue({
|
||||
loading: false,
|
||||
isModuleEnabled: false,
|
||||
listItems: [],
|
||||
|
|
|
@ -164,10 +164,10 @@ const OverviewComponent = () => {
|
|||
<EuiFlexItem grow={1}>
|
||||
{riskyHostsEnabled && (
|
||||
<RiskyHostLinks
|
||||
deleteQuery={deleteQuery}
|
||||
from={from}
|
||||
setQuery={setQuery}
|
||||
to={to}
|
||||
timerange={{
|
||||
from,
|
||||
to,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -435,6 +435,7 @@ exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should
|
|||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
hostRisk={null}
|
||||
isAlert={false}
|
||||
isDraggable={false}
|
||||
loading={true}
|
||||
|
@ -954,6 +955,7 @@ Array [
|
|||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
hostRisk={null}
|
||||
isAlert={false}
|
||||
isDraggable={false}
|
||||
loading={true}
|
||||
|
@ -1990,6 +1992,7 @@ Array [
|
|||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
hostRisk={null}
|
||||
isAlert={false}
|
||||
isDraggable={false}
|
||||
loading={true}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { BrowserFields } from '../../../../common/containers/source';
|
|||
import { EventDetails } from '../../../../common/components/event_details/event_details';
|
||||
import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
||||
import * as i18n from './translations';
|
||||
import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
|
||||
export type HandleOnEventClosed = () => void;
|
||||
interface Props {
|
||||
|
@ -34,6 +35,7 @@ interface Props {
|
|||
messageHeight?: number;
|
||||
timelineTabType: TimelineTabs | 'flyout';
|
||||
timelineId: string;
|
||||
hostRisk: HostRisk | null;
|
||||
}
|
||||
|
||||
interface ExpandableEventTitleProps {
|
||||
|
@ -90,6 +92,7 @@ export const ExpandableEvent = React.memo<Props>(
|
|||
isDraggable,
|
||||
loading,
|
||||
detailsData,
|
||||
hostRisk,
|
||||
}) => {
|
||||
if (!event.eventId) {
|
||||
return <EuiTextColor color="subdued">{i18n.EVENT_DETAILS_PLACEHOLDER}</EuiTextColor>;
|
||||
|
@ -110,6 +113,7 @@ export const ExpandableEvent = React.memo<Props>(
|
|||
isDraggable={isDraggable}
|
||||
timelineId={timelineId}
|
||||
timelineTabType={timelineTabType}
|
||||
hostRisk={hostRisk}
|
||||
/>
|
||||
</StyledEuiFlexItem>
|
||||
</StyledFlexGroup>
|
||||
|
|
|
@ -34,6 +34,7 @@ import { TimelineNonEcsData } from '../../../../../common';
|
|||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { EventDetailsFooter } from './footer';
|
||||
import { EntityType } from '../../../../../../timelines/common';
|
||||
import { useHostsRiskScore } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score';
|
||||
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflow {
|
||||
|
@ -124,6 +125,10 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
[detailsData]
|
||||
);
|
||||
|
||||
const hostRisk = useHostsRiskScore({
|
||||
hostName,
|
||||
});
|
||||
|
||||
const backToAlertDetailsLink = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
|
@ -192,6 +197,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
timelineTabType="flyout"
|
||||
hostRisk={hostRisk}
|
||||
/>
|
||||
)}
|
||||
</StyledEuiFlyoutBody>
|
||||
|
@ -224,6 +230,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
timelineTabType={tabType}
|
||||
hostRisk={hostRisk}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,7 +10,8 @@ import { HostsQueries, HostsKpiQueries } from '../../../../../common/search_stra
|
|||
import { allHosts } from './all';
|
||||
import { hostDetails } from './details';
|
||||
import { hostOverview } from './overview';
|
||||
import { riskyHosts } from './risky_hosts';
|
||||
|
||||
import { riskScore } from './risk_score';
|
||||
import { firstOrLastSeenHost } from './last_first_seen';
|
||||
import { uncommonProcesses } from './uncommon_processes';
|
||||
import { authentications, authenticationsEntities } from './authentications';
|
||||
|
@ -27,7 +28,7 @@ jest.mock('./authentications');
|
|||
jest.mock('./kpi/authentications');
|
||||
jest.mock('./kpi/hosts');
|
||||
jest.mock('./kpi/unique_ips');
|
||||
jest.mock('./risky_hosts');
|
||||
jest.mock('./risk_score');
|
||||
|
||||
describe('hostsFactory', () => {
|
||||
test('should include correct apis', () => {
|
||||
|
@ -39,7 +40,7 @@ describe('hostsFactory', () => {
|
|||
[HostsQueries.uncommonProcesses]: uncommonProcesses,
|
||||
[HostsQueries.authentications]: authentications,
|
||||
[HostsQueries.authenticationsEntities]: authenticationsEntities,
|
||||
[HostsQueries.riskyHosts]: riskyHosts,
|
||||
[HostsQueries.hostsRiskScore]: riskScore,
|
||||
[HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications,
|
||||
[HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities,
|
||||
[HostsKpiQueries.kpiHosts]: hostsKpiHosts,
|
||||
|
|
|
@ -21,7 +21,7 @@ import { authentications, authenticationsEntities } from './authentications';
|
|||
import { hostsKpiAuthentications, hostsKpiAuthenticationsEntities } from './kpi/authentications';
|
||||
import { hostsKpiHosts, hostsKpiHostsEntities } from './kpi/hosts';
|
||||
import { hostsKpiUniqueIps, hostsKpiUniqueIpsEntities } from './kpi/unique_ips';
|
||||
import { riskyHosts } from './risky_hosts';
|
||||
import { riskScore } from './risk_score';
|
||||
|
||||
export const hostsFactory: Record<
|
||||
HostsQueries | HostsKpiQueries,
|
||||
|
@ -35,7 +35,7 @@ export const hostsFactory: Record<
|
|||
[HostsQueries.uncommonProcesses]: uncommonProcesses,
|
||||
[HostsQueries.authentications]: authentications,
|
||||
[HostsQueries.authenticationsEntities]: authenticationsEntities,
|
||||
[HostsQueries.riskyHosts]: riskyHosts,
|
||||
[HostsQueries.hostsRiskScore]: riskScore,
|
||||
[HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications,
|
||||
[HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities,
|
||||
[HostsKpiQueries.kpiHosts]: hostsKpiHosts,
|
||||
|
|
|
@ -6,23 +6,23 @@
|
|||
*/
|
||||
|
||||
import { SecuritySolutionFactory } from '../../types';
|
||||
import { HostsQueries } from '../../../../../../common';
|
||||
import {
|
||||
HostsRiskScoreRequestOptions,
|
||||
HostsQueries,
|
||||
HostsRiskScoreStrategyResponse,
|
||||
} from '../../../../../../common';
|
||||
import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common';
|
||||
import { inspectStringifyObject } from '../../../../../utils/build_query';
|
||||
import { buildRiskyHostsQuery } from './query.risky_hosts.dsl';
|
||||
import {
|
||||
HostsRiskyHostsRequestOptions,
|
||||
HostsRiskyHostsStrategyResponse,
|
||||
} from '../../../../../../common/search_strategy/security_solution/hosts/risky_hosts';
|
||||
import { buildHostsRiskScoreQuery } from './query.hosts_risk.dsl';
|
||||
|
||||
export const riskyHosts: SecuritySolutionFactory<HostsQueries.riskyHosts> = {
|
||||
buildDsl: (options: HostsRiskyHostsRequestOptions) => buildRiskyHostsQuery(options),
|
||||
export const riskScore: SecuritySolutionFactory<HostsQueries.hostsRiskScore> = {
|
||||
buildDsl: (options: HostsRiskScoreRequestOptions) => buildHostsRiskScoreQuery(options),
|
||||
parse: async (
|
||||
options: HostsRiskyHostsRequestOptions,
|
||||
options: HostsRiskScoreRequestOptions,
|
||||
response: IEsSearchResponse<unknown>
|
||||
): Promise<HostsRiskyHostsStrategyResponse> => {
|
||||
): Promise<HostsRiskScoreStrategyResponse> => {
|
||||
const inspect = {
|
||||
dsl: [inspectStringifyObject(buildRiskyHostsQuery(options))],
|
||||
dsl: [inspectStringifyObject(buildHostsRiskScoreQuery(options))],
|
||||
};
|
||||
|
||||
return {
|
|
@ -5,26 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HostsRiskyHostsRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts/risky_hosts';
|
||||
import { createQueryFilterClauses } from '../../../../../utils/build_query';
|
||||
import { HostsRiskScoreRequestOptions } from '../../../../../../common';
|
||||
|
||||
export const buildRiskyHostsQuery = ({
|
||||
filterQuery,
|
||||
timerange: { from, to },
|
||||
export const buildHostsRiskScoreQuery = ({
|
||||
timerange,
|
||||
hostName,
|
||||
defaultIndex,
|
||||
}: HostsRiskyHostsRequestOptions) => {
|
||||
const filter = [
|
||||
...createQueryFilterClauses(filterQuery),
|
||||
{
|
||||
}: HostsRiskScoreRequestOptions) => {
|
||||
const filter = [];
|
||||
|
||||
if (timerange) {
|
||||
filter.push({
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
gte: timerange.from,
|
||||
lte: timerange.to,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (hostName) {
|
||||
filter.push({ term: { 'host.name': hostName } });
|
||||
}
|
||||
|
||||
const dslQuery = {
|
||||
index: defaultIndex,
|
|
@ -22351,9 +22351,7 @@
|
|||
"xpack.securitySolution.eventDetails.copyToClipboard": "クリップボードにコピー",
|
||||
"xpack.securitySolution.eventDetails.copyToClipboardTooltip": "クリップボードにコピー",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent": "このフィールド値は、作成したルールの脅威インテリジェンス指標と一致しました。",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle": "脅威一致が検出されました",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "このフィールド値には脅威インテリジェンスソースの別の情報があります。",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle": "Threat Intelligenceで拡張",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.providerPreposition": "開始",
|
||||
"xpack.securitySolution.eventDetails.description": "説明",
|
||||
"xpack.securitySolution.eventDetails.field": "フィールド",
|
||||
|
|
|
@ -22700,9 +22700,7 @@
|
|||
"xpack.securitySolution.eventDetails.copyToClipboard": "复制到剪贴板",
|
||||
"xpack.securitySolution.eventDetails.copyToClipboardTooltip": "复制到剪贴板",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent": "此字段值使用您创建的规则匹配威胁情报指标。",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle": "检测到威胁匹配",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "此字段值具有威胁情报源提供的其他信息。",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle": "已使用威胁情报扩充",
|
||||
"xpack.securitySolution.eventDetails.ctiSummary.providerPreposition": "来自",
|
||||
"xpack.securitySolution.eventDetails.description": "描述",
|
||||
"xpack.securitySolution.eventDetails.field": "字段",
|
||||
|
|
Loading…
Reference in a new issue