[Security Solution] Flyout overview hover actions (#106362)

* 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
This commit is contained in:
Angela Chuang 2021-07-22 16:11:32 +01:00 committed by GitHub
parent 5b0d679c60
commit 745db3063a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 91 deletions

View file

@ -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']) => (
<FormattedFieldValue
contextId={`alert-details-value-formatted-field-value-${contextId}-${eventId}-${fieldName}-${value}`}
eventId={eventId}
fieldName={fieldName}
fieldType={fieldType}
value={value}
linkValue={linkValue}
/>
);
timelineId,
values,
}: AlertSummaryRow['description']) => {
if (isEmpty(values)) {
return <StyledEmptyComponent>{getEmptyValue()}</StyledEmptyComponent>;
}
const eventFieldsData = {
...data,
...(fieldFromBrowserField ? fieldFromBrowserField : {}),
} as EventFieldsData;
return (
<>
<FieldValueCell
contextId={timelineId}
data={eventFieldsData}
eventId={eventId}
fieldFromBrowserField={fieldFromBrowserField}
linkValue={linkValue}
values={values}
/>
<ActionCell
contextId={timelineId}
data={eventFieldsData}
eventId={eventId}
fieldFromBrowserField={fieldFromBrowserField}
linkValue={linkValue}
timelineId={timelineId}
values={values}
/>
</>
);
};
const getSummaryRows = ({
data,
@ -120,25 +149,45 @@ const getSummaryRows = ({
return data != null
? tableFields.reduce<SummaryRow[]>((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 (
<>
<EuiSpacer size="l" />
<SummaryView
summaryColumns={summaryColumns}
summaryRows={isEndpointAlert ? summaryRowsWithAgentStatus : summaryRows}
title={title}
/>
<SummaryView summaryColumns={summaryColumns} summaryRows={summaryRows} title={title} />
{maybeRule?.note && (
<>
<EuiHorizontalRule />

View file

@ -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<Record<string, Partial<BrowserField>>>;
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: '',
},
{

View file

@ -19,8 +19,9 @@ interface Props {
data: EventFieldsData;
disabled?: boolean;
eventId: string;
fieldFromBrowserField: Readonly<Record<string, Partial<BrowserField>>>;
getLinkValue: (field: string) => string | null;
fieldFromBrowserField?: Readonly<Record<string, Partial<BrowserField>>>;
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<Props> = React.memo(
eventId,
fieldFromBrowserField,
getLinkValue,
linkValue,
onFilterAdded,
timelineId,
toggleColumn,
@ -47,7 +49,7 @@ export const ActionCell: React.FC<Props> = React.memo(
fieldFromBrowserField,
fieldType: data.type,
isObjectArray: data.isObjectArray,
linkValue: getLinkValue(data.field),
linkValue: (getLinkValue && getLinkValue(data.field)) ?? linkValue,
values,
});

View file

@ -17,8 +17,9 @@ export interface FieldValueCellProps {
contextId: string;
data: EventFieldsData;
eventId: string;
fieldFromBrowserField: Readonly<Record<string, Partial<BrowserField>>>;
getLinkValue: (field: string) => string | null;
fieldFromBrowserField?: Readonly<Record<string, Partial<BrowserField>>>;
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}
/>
)}
</div>

View file

@ -31,7 +31,7 @@ export interface UseActionCellDataProvider {
eventId?: string;
field: string;
fieldFormat?: string;
fieldFromBrowserField: Readonly<Record<string, Partial<BrowserField>>>;
fieldFromBrowserField?: Readonly<Record<string, Partial<BrowserField>>>;
fieldType?: string;
isObjectArray?: boolean;
linkValue?: string | null;

View file

@ -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<Props> = React.memo(
? SourcererScopeName.detections
: SourcererScopeName.default;
const { browserFields, indexPattern } = useSourcererScope(activeScope);
const button = (
<EuiButtonIcon
aria-label={SHOW_TOP(field)}
className="securitySolution__hoverActionButton"
data-test-subj="show-top-field"
iconSize="s"
iconType="visBarVertical"
onClick={onClick}
/>
const button = useMemo(
() => (
<EuiButtonIcon
aria-label={SHOW_TOP(field)}
className="securitySolution__hoverActionButton"
data-test-subj="show-top-field"
iconSize="s"
iconType="visBarVertical"
onClick={onClick}
/>
),
[field, onClick]
);
return showTopN ? (
<EuiPopover button={button} isOpen={showTopN} closePopover={onClick}>
@ -80,14 +83,7 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
/>
}
>
<EuiButtonIcon
aria-label={SHOW_TOP(field)}
className="securitySolution__hoverActionButton"
data-test-subj="show-top-field"
iconSize="s"
iconType="visBarVertical"
onClick={onClick}
/>
{button}
</EuiToolTip>
);
}

View file

@ -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<Props> = React.memo(
]
);
const showFilters = !showTopN && values != null;
const showFilters = values != null;
return (
<StyledHoverActionsContainer onKeyDown={onKeyDown} ref={panelRef} $showTopN={showTopN}>
@ -342,7 +342,7 @@ export const HoverActions: React.FC<Props> = React.memo(
value={values}
/>
)}
{!showTopN && (
{showFilters && (
<CopyButton
data-test-subj="hover-actions-copy-button"
field={field}