mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
5b0d679c60
commit
745db3063a
7 changed files with 114 additions and 91 deletions
|
@ -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 />
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue