From 8a6edd8165eb3191213cf5fcd631d3d6dc72fdb0 Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Sat, 24 Jul 2021 11:11:37 +0100
Subject: [PATCH] [Security Solution] Flyout overview hover actions (#106362)
(#106654)
* flyout-overview
* integrate with hover actions
* fix types
* fix types
* move TopN into a popover
* fix types
* fix up
* update field width
* fix unit tests
* fix agent status field
---
.../event_details/alert_summary_view.tsx | 135 ++++++++++--------
.../components/event_details/helpers.tsx | 12 +-
.../event_details/table/action_cell.tsx | 8 +-
.../event_details/table/field_value_cell.tsx | 8 +-
.../table/use_action_cell_data_provider.ts | 2 +-
.../hover_actions/actions/show_top_n.tsx | 32 ++---
.../common/components/hover_actions/index.tsx | 8 +-
7 files changed, 114 insertions(+), 91 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx
index 841aa6840cc0b..501ef78d550f9 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx
@@ -6,13 +6,11 @@
*/
import { EuiBasicTableColumn, EuiSpacer, EuiHorizontalRule, EuiTitle, EuiText } from '@elastic/eui';
-import { get, getOr, find } from 'lodash/fp';
+import { get, getOr, find, isEmpty } from 'lodash/fp';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import * as i18n from './translations';
-import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
-import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
import {
ALERTS_HEADERS_RISK_SCORE,
@@ -25,6 +23,7 @@ import {
TIMESTAMP,
} 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';
@@ -35,12 +34,21 @@ import { useRuleWithFallback } from '../../../detections/containers/detection_en
import { MarkdownRenderer } from '../markdown_editor';
import { LineClamp } from '../line_clamp';
import { endpointAlertCheck } 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 { EventFieldsData } from './types';
export const Indent = styled.div`
padding: 0 8px;
word-break: break-word;
`;
+const StyledEmptyComponent = styled.div`
+ padding: ${(props) => `${props.theme.eui.paddingSizes.xs} 0`};
+`;
+
const fields = [
{ id: 'signal.status', label: SIGNAL_STATUS },
{ id: '@timestamp', label: TIMESTAMP },
@@ -52,7 +60,7 @@ const fields = [
{ id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY },
{ id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE },
{ id: 'host.name' },
- { id: 'agent.status' },
+ { 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 },
@@ -76,22 +84,43 @@ const networkFields = [
];
const getDescription = ({
- contextId,
+ data,
eventId,
- fieldName,
- value,
- fieldType = '',
+ fieldFromBrowserField,
linkValue,
-}: AlertSummaryRow['description']) => (
-
-);
+ timelineId,
+ values,
+}: AlertSummaryRow['description']) => {
+ if (isEmpty(values)) {
+ return {getEmptyValue()};
+ }
+
+ const eventFieldsData = {
+ ...data,
+ ...(fieldFromBrowserField ? fieldFromBrowserField : {}),
+ } as EventFieldsData;
+ return (
+ <>
+
+
+ >
+ );
+};
const getSummaryRows = ({
data,
@@ -120,25 +149,45 @@ const getSummaryRows = ({
return data != null
? tableFields.reduce((acc, item) => {
+ const initialDescription = {
+ contextId: timelineId,
+ eventId,
+ value: null,
+ fieldType: 'string',
+ linkValue: undefined,
+ timelineId,
+ };
const field = data.find((d) => d.field === item.id);
if (!field) {
- return acc;
+ 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 fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string;
+ const category = field.category ?? '';
+ const fieldName = field.field ?? '';
+
+ const browserField = get([category, 'fields', fieldName], browserFields);
const description = {
- contextId: timelineId,
- eventId,
- fieldName: item.id,
- value,
- fieldType: item.fieldType ?? fieldType,
+ ...initialDescription,
+ data: { ...field, ...(item.overrideField ? { field: item.overrideField } : {}) },
+ values: field.values,
linkValue: linkValue ?? undefined,
+ fieldFromBrowserField: browserField,
};
+ if (item.id === 'agent.id' && !endpointAlertCheck({ data })) {
+ return acc;
+ }
+
if (item.id === 'signal.threshold_result.terms') {
try {
const terms = getOr(null, 'originalValue', field);
@@ -149,14 +198,14 @@ const getSummaryRows = ({
title: `${entry.field} [threshold]`,
description: {
...description,
- value: entry.value,
+ values: [entry.value],
},
};
}
);
return [...acc, ...thresholdTerms];
} catch (err) {
- return acc;
+ return [...acc];
}
}
@@ -169,7 +218,7 @@ const getSummaryRows = ({
title: ALERTS_HEADERS_THRESHOLD_CARDINALITY,
description: {
...description,
- value: `count(${parsedValue.field}) == ${parsedValue.value}`,
+ values: [`count(${parsedValue.field}) == ${parsedValue.value}`],
},
},
];
@@ -205,28 +254,6 @@ const AlertSummaryViewComponent: React.FC<{
timelineId,
]);
- const isEndpointAlert = useMemo(() => {
- return endpointAlertCheck({ data });
- }, [data]);
-
- const endpointId = useMemo(() => {
- const findAgentId = find({ category: 'agent', field: 'agent.id' }, data)?.values;
- return findAgentId ? findAgentId[0] : '';
- }, [data]);
-
- const agentStatusRow = {
- title: i18n.AGENT_STATUS,
- description: {
- contextId: timelineId,
- eventId,
- fieldName: 'agent.status',
- value: endpointId,
- linkValue: undefined,
- },
- };
-
- const summaryRowsWithAgentStatus = [...summaryRows, agentStatusRow];
-
const ruleId = useMemo(() => {
const item = data.find((d) => d.field === 'signal.rule.id');
return Array.isArray(item?.originalValue)
@@ -238,11 +265,7 @@ const AlertSummaryViewComponent: React.FC<{
return (
<>
-
+
{maybeRule?.note && (
<>
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
index 2b300789c4d14..ecfa243f89246 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
@@ -23,7 +23,7 @@ import {
} from '../../../timelines/components/timeline/body/constants';
import * as i18n from './translations';
-import { ColumnHeaderOptions } from '../../../../common';
+import { ColumnHeaderOptions, TimelineEventsDetailsItem } from '../../../../common';
/**
* Defines the behavior of the search input that appears above the table of data
@@ -55,12 +55,12 @@ export interface Item {
export interface AlertSummaryRow {
title: string;
description: {
- contextId: string;
+ data: TimelineEventsDetailsItem;
eventId: string;
- fieldName: string;
- value: string;
- fieldType: string;
+ fieldFromBrowserField?: Readonly>>;
linkValue: string | undefined;
+ timelineId: string;
+ values: string[] | null | undefined;
};
}
@@ -213,7 +213,7 @@ export const getSummaryColumns = (
field: 'title',
truncateText: false,
render: getTitle,
- width: '160px',
+ width: '33%',
name: '',
},
{
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx
index 795ecb266b092..f5cf600e281ad 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx
@@ -19,8 +19,9 @@ interface Props {
data: EventFieldsData;
disabled?: boolean;
eventId: string;
- fieldFromBrowserField: Readonly>>;
- getLinkValue: (field: string) => string | null;
+ fieldFromBrowserField?: Readonly>>;
+ getLinkValue?: (field: string) => string | null;
+ linkValue?: string | null | undefined;
onFilterAdded?: () => void;
timelineId?: string;
toggleColumn?: (column: ColumnHeaderOptions) => void;
@@ -34,6 +35,7 @@ export const ActionCell: React.FC = React.memo(
eventId,
fieldFromBrowserField,
getLinkValue,
+ linkValue,
onFilterAdded,
timelineId,
toggleColumn,
@@ -47,7 +49,7 @@ export const ActionCell: React.FC = React.memo(
fieldFromBrowserField,
fieldType: data.type,
isObjectArray: data.isObjectArray,
- linkValue: getLinkValue(data.field),
+ linkValue: (getLinkValue && getLinkValue(data.field)) ?? linkValue,
values,
});
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx
index b6524a8c9c895..2ac0ca23ca8c1 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx
@@ -17,8 +17,9 @@ export interface FieldValueCellProps {
contextId: string;
data: EventFieldsData;
eventId: string;
- fieldFromBrowserField: Readonly>>;
- getLinkValue: (field: string) => string | null;
+ fieldFromBrowserField?: Readonly>>;
+ getLinkValue?: (field: string) => string | null;
+ linkValue?: string | null | undefined;
values: string[] | null | undefined;
}
@@ -29,6 +30,7 @@ export const FieldValueCell = React.memo(
eventId,
fieldFromBrowserField,
getLinkValue,
+ linkValue,
values,
}: FieldValueCellProps) => {
return (
@@ -55,7 +57,7 @@ export const FieldValueCell = React.memo(
fieldType={data.type}
isObjectArray={data.isObjectArray}
value={value}
- linkValue={getLinkValue(data.field)}
+ linkValue={(getLinkValue && getLinkValue(data.field)) ?? linkValue}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts
index e580ae6c1fdef..fbe9767759d28 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts
@@ -31,7 +31,7 @@ export interface UseActionCellDataProvider {
eventId?: string;
field: string;
fieldFormat?: string;
- fieldFromBrowserField: Readonly>>;
+ fieldFromBrowserField?: Readonly>>;
fieldType?: string;
isObjectArray?: boolean;
linkValue?: string | null;
diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx
index 6e284289243f0..0fc8a74084521 100644
--- a/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/actions/show_top_n.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
import { EuiButtonIcon, EuiPopover, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { StatefulTopN } from '../../top_n';
@@ -44,15 +44,18 @@ export const ShowTopNButton: React.FC = React.memo(
? SourcererScopeName.detections
: SourcererScopeName.default;
const { browserFields, indexPattern } = useSourcererScope(activeScope);
- const button = (
-
+ const button = useMemo(
+ () => (
+
+ ),
+ [field, onClick]
);
return showTopN ? (
@@ -80,14 +83,7 @@ export const ShowTopNButton: React.FC = React.memo(
/>
}
>
-
+ {button}
);
}
diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx
index a7fdb26a525fb..31bdf78626e7c 100644
--- a/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/index.tsx
@@ -39,7 +39,7 @@ export const AdditionalContent = styled.div`
AdditionalContent.displayName = 'AdditionalContent';
const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>`
- padding: ${(props) => (props.$showTopN ? 'none' : `0 ${props.theme.eui.paddingSizes.s}`)};
+ padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`};
display: flex;
&:focus-within {
@@ -58,7 +58,7 @@ const StyledHoverActionsContainer = styled.div<{ $showTopN: boolean }>`
.timelines__hoverActionButton,
.securitySolution__hoverActionButton {
- opacity: 0;
+ opacity: ${(props) => (props.$showTopN ? 1 : 0)};
&:focus {
opacity: 1;
@@ -268,7 +268,7 @@ export const HoverActions: React.FC = React.memo(
]
);
- const showFilters = !showTopN && values != null;
+ const showFilters = values != null;
return (
@@ -342,7 +342,7 @@ export const HoverActions: React.FC = React.memo(
value={values}
/>
)}
- {!showTopN && (
+ {showFilters && (