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 && (