Skip to content

Commit

Permalink
[Security Solution] Flyout overview hover actions (#106362) (#106654)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
angorayc authored Jul 24, 2021
1 parent 17933a5 commit 8a6edd8
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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];
}
}

Expand All @@ -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}`],
},
},
];
Expand Down Expand Up @@ -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)
Expand All @@ -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 />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};
}

Expand Down Expand Up @@ -213,7 +213,7 @@ export const getSummaryColumns = (
field: 'title',
truncateText: false,
render: getTitle,
width: '160px',
width: '33%',
name: '',
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +35,7 @@ export const ActionCell: React.FC<Props> = React.memo(
eventId,
fieldFromBrowserField,
getLinkValue,
linkValue,
onFilterAdded,
timelineId,
toggleColumn,
Expand All @@ -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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -29,6 +30,7 @@ export const FieldValueCell = React.memo(
eventId,
fieldFromBrowserField,
getLinkValue,
linkValue,
values,
}: FieldValueCellProps) => {
return (
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -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>
);
}
Expand Down
Loading

0 comments on commit 8a6edd8

Please sign in to comment.