Skip to content

Commit

Permalink
[Security Solution][Investigations] Alert flyout UX updates (pt. 1) (#…
Browse files Browse the repository at this point in the history
…120347)

* feat: Move timestamp from summary to below the title

* refactor: creat reusable getEnrichedFieldInfo

This method can be used in different places to enrich the field data.

* feat: make unpadded/unsized version of ActionCell available

Ideally, ActionCell and HoverActions would not have padding and width declaration. This could be part of a future refactor. For now, a version with padding and size information is all that is needed.

* feat: add OverviewCards w/ severity, risk score and rule name

* feat: add status to overview cards

* refactor: use FormattedFieldValue instead of RuleStatus directly

* fix: limit height of the overview cards

* fix: clamp content to 2 lines

* chore: add displayName

* feat: Add interactive popover to status badge

* chore: remove signal status from document summary

* feat: Remove rule link and headline from reason component

* feat: Add table-tab pivot link

* feat: close alert flyout after status change

* test: fix snapshots

* chore: remove unused imports

* chore: use correct padding in context menu

* chore: split over cards into multiple files

* chore: use shared severity badge

* chore: revert back to plain risk score text

* chore: rename and move overview

* fix: fix alignment between actions and content

* fix: fix types in test

* chore: remove unused import

* chore: useMemo & useCallback

* chore: import type

* feat: add iconType, iconSide and onClickArialabel to rule status

* feat: add hover actions to the alert status overview card

* fix: use correct data

* fix: action cell did not look good on small screens

Now the action cell slides in similar to how the action buttons slide in in a data grid.

* fix: use different card layout based on container width

* fix: use new Severity type

* fix: align children centered

* test: add popover button tests

* test: add overview card test

* test: test overview cards

* fix: prevent rendering of two cards in two ingle rows

* fix: change i18n key to prevent a duplicate key

* chore: remove unused translations

* nit: use less vertical screen estate

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
janmonschke and kibanamachine authored Dec 10, 2021
1 parent e60c6e1 commit 0b20010
Show file tree
Hide file tree
Showing 27 changed files with 1,321 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const props = {
browserFields: mockBrowserFields,
eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31',
timelineId: 'detections-page',
title: '',
goToTable: jest.fn(),
};

describe('AlertSummaryView', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui';
import { EuiBasicTableColumn } from '@elastic/eui';
import React, { useMemo } from 'react';

import { BrowserFields } from '../../../../common/search_strategy/index_fields';
Expand Down Expand Up @@ -60,18 +60,21 @@ const AlertSummaryViewComponent: React.FC<{
eventId: string;
isDraggable?: boolean;
timelineId: string;
title?: string;
}> = ({ browserFields, data, eventId, isDraggable, timelineId, title }) => {
title: string;
goToTable: () => void;
}> = ({ browserFields, data, eventId, isDraggable, timelineId, title, goToTable }) => {
const summaryRows = useMemo(
() => getSummaryRows({ browserFields, data, eventId, isDraggable, timelineId }),
[browserFields, data, eventId, isDraggable, timelineId]
);

return (
<>
<EuiSpacer size="s" />
<SummaryView summaryColumns={summaryColumns} summaryRows={summaryRows} title={title} />
</>
<SummaryView
summaryColumns={summaryColumns}
summaryRows={summaryRows}
title={title}
goToTable={goToTable}
/>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ describe('EventDetails', () => {
timelineId: 'test',
eventView: EventsViewType.summaryView,
hostRisk: { fields: [], loading: true },
indexName: 'test',
handleOnEventClosed: jest.fn(),
rawEventData,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import {
EuiHorizontalRule,
EuiTabbedContent,
EuiTabbedContentTab,
EuiSpacer,
Expand Down Expand Up @@ -39,7 +40,9 @@ import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker';
import { Reason } from './reason';

import { InvestigationGuideView } from './investigation_guide_view';

import { HostRisk } from '../../containers/hosts_risk/use_hosts_risk_score';
import { Overview } from './overview';

type EventViewTab = EuiTabbedContentTab;

Expand All @@ -59,12 +62,14 @@ interface Props {
browserFields: BrowserFields;
data: TimelineEventsDetailsItem[];
id: string;
indexName: string;
isAlert: boolean;
isDraggable?: boolean;
rawEventData: object | undefined;
timelineTabType: TimelineTabs | 'flyout';
timelineId: string;
hostRisk: HostRisk | null;
handleOnEventClosed: () => void;
}

export const Indent = styled.div`
Expand Down Expand Up @@ -105,18 +110,21 @@ const EventDetailsComponent: React.FC<Props> = ({
browserFields,
data,
id,
indexName,
isAlert,
isDraggable,
rawEventData,
timelineId,
timelineTabType,
hostRisk,
handleOnEventClosed,
}) => {
const [selectedTabId, setSelectedTabId] = useState<EventViewId>(EventsViewType.summaryView);
const handleTabClick = useCallback(
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId),
[setSelectedTabId]
[]
);
const goToTableTab = useCallback(() => setSelectedTabId(EventsViewType.tableView), []);

const eventFields = useMemo(() => getEnrichmentFields(data), [data]);
const existingEnrichments = useMemo(
Expand Down Expand Up @@ -152,7 +160,19 @@ const EventDetailsComponent: React.FC<Props> = ({
name: i18n.OVERVIEW,
content: (
<>
<EuiSpacer size="m" />
<Overview
browserFields={browserFields}
contextId={timelineId}
data={data}
eventId={id}
indexName={indexName}
timelineId={timelineId}
handleOnEventClosed={handleOnEventClosed}
/>
<EuiSpacer size="l" />
<Reason eventId={id} data={data} />
<EuiHorizontalRule />
<AlertSummaryView
{...{
data,
Expand All @@ -162,6 +182,7 @@ const EventDetailsComponent: React.FC<Props> = ({
timelineId,
title: i18n.DUCOMENT_SUMMARY,
}}
goToTable={goToTableTab}
/>

{(enrichmentCount > 0 || hostRisk) && (
Expand All @@ -188,8 +209,9 @@ const EventDetailsComponent: React.FC<Props> = ({
}
: undefined,
[
isAlert,
id,
indexName,
isAlert,
data,
browserFields,
isDraggable,
Expand All @@ -198,6 +220,8 @@ const EventDetailsComponent: React.FC<Props> = ({
allEnrichments,
isEnrichmentsLoading,
hostRisk,
goToTableTab,
handleOnEventClosed,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,31 @@
* 2.0.
*/

import { get, getOr, find, isEmpty } from 'lodash/fp';
import { 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 { getEnrichedFieldInfo, SummaryRow } from './helpers';
import { EventSummaryField } from './types';
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: 'kibana.alert.workflow_status', label: SIGNAL_STATUS },
{ id: '@timestamp', label: TIMESTAMP },
{
id: SIGNAL_RULE_NAME_FIELD_NAME,
linkField: 'kibana.alert.rule.uuid',
label: ALERTS_HEADERS_RULE,
},
{ id: 'kibana.alert.rule.severity', label: ALERTS_HEADERS_SEVERITY },
{ id: 'kibana.alert.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' },
Expand Down Expand Up @@ -151,50 +129,34 @@ export const getSummaryRows = ({
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)) {
? tableFields.reduce<SummaryRow[]>((acc, field) => {
const item = data.find((d) => d.field === field.id);
if (!item || isEmpty(item?.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);
field.linkField != null && data.find((d) => d.field === field.linkField);
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,
...getEnrichedFieldInfo({
item,
linkValueField: linkValueField || undefined,
contextId: timelineId,
timelineId,
browserFields,
eventId,
field,
}),
isDraggable,
};

if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) {
if (field.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) {
return acc;
}

if (item.id === 'kibana.alert.threshold_result.terms') {
if (field.id === 'kibana.alert.threshold_result.terms') {
try {
const terms = getOr(null, 'originalValue', field);
const terms = getOr(null, 'originalValue', item);
const parsedValue = terms.map((term: string) => JSON.parse(term));
const thresholdTerms = (parsedValue ?? []).map(
(entry: { field: string; value: string }) => {
Expand All @@ -213,8 +175,9 @@ export const getSummaryRows = ({
}
}

if (item.id === 'kibana.alert.threshold_result.cardinality') {
if (field.id === 'kibana.alert.threshold_result.cardinality') {
try {
const value = getOr(null, 'originalValue.0', field);
const parsedValue = JSON.parse(value);
return [
...acc,
Expand All @@ -234,7 +197,7 @@ export const getSummaryRows = ({
return [
...acc,
{
title: item.label ?? item.id,
title: field.label ?? field.id,
description,
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
DEFAULT_DATE_COLUMN_MIN_WIDTH,
DEFAULT_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
import { FieldsData } from './types';
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import type { EnrichedFieldInfo, EventSummaryField } from './types';

import * as i18n from './translations';
import { ColumnHeaderOptions } from '../../../../common/types';
Expand Down Expand Up @@ -56,14 +57,8 @@ export interface Item {

export interface AlertSummaryRow {
title: string;
description: {
data: FieldsData;
eventId: string;
description: EnrichedFieldInfo & {
isDraggable?: boolean;
fieldFromBrowserField?: BrowserField;
linkValue: string | undefined;
timelineId: string;
values: string[] | null | undefined;
};
}

Expand Down Expand Up @@ -232,3 +227,47 @@ export const getSummaryColumns = (
},
];
};

export function getEnrichedFieldInfo({
browserFields,
contextId,
eventId,
field,
item,
linkValueField,
timelineId,
}: {
browserFields: BrowserFields;
contextId: string;
item: TimelineEventsDetailsItem;
eventId: string;
field?: EventSummaryField;
timelineId: string;
linkValueField?: TimelineEventsDetailsItem;
}): EnrichedFieldInfo {
const fieldInfo = {
contextId,
eventId,
fieldType: 'string',
linkValue: undefined,
timelineId,
};
const linkValue = getOr(null, 'originalValue.0', linkValueField);
const category = item.category ?? '';
const fieldName = item.field ?? '';

const browserField = get([category, 'fields', fieldName], browserFields);
const overrideField = field?.overrideField;
return {
...fieldInfo,
data: {
field: overrideField ?? fieldName,
format: browserField?.format ?? '',
type: browserField?.type ?? '',
isObjectArray: item.isObjectArray,
},
values: item.values,
linkValue: linkValue ?? undefined,
fieldFromBrowserField: browserField,
};
}
Loading

0 comments on commit 0b20010

Please sign in to comment.