From 0b200103aa0e950b27af3336bc40ecc250d61613 Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Fri, 10 Dec 2021 11:30:01 +0100 Subject: [PATCH] [Security Solution][Investigations] Alert flyout UX updates (pt. 1) (#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 <42973632+kibanamachine@users.noreply.github.com> --- .../event_details/alert_summary_view.test.tsx | 2 + .../event_details/alert_summary_view.tsx | 17 +- .../event_details/event_details.test.tsx | 2 + .../event_details/event_details.tsx | 28 +- .../event_details/get_alert_summary_rows.tsx | 83 ++--- .../components/event_details/helpers.tsx | 55 +++- .../__snapshots__/index.test.tsx.snap | 311 ++++++++++++++++++ .../event_details/overview/index.test.tsx | 198 +++++++++++ .../event_details/overview/index.tsx | 210 ++++++++++++ .../overview/overview_card.test.tsx | 71 ++++ .../event_details/overview/overview_card.tsx | 99 ++++++ .../overview/status_popover_button.test.tsx | 82 +++++ .../overview/status_popover_button.tsx | 81 +++++ .../components/event_details/reason.tsx | 57 +--- .../components/event_details/summary_view.tsx | 41 ++- .../event_details/table/action_cell.tsx | 14 +- .../components/event_details/translations.ts | 7 + .../common/components/event_details/types.ts | 19 ++ .../common/components/hover_actions/index.tsx | 17 +- .../components/alerts_table/translations.ts | 21 +- .../__snapshots__/index.test.tsx.snap | 5 + .../event_details/expandable_event.tsx | 25 +- .../side_panel/event_details/index.tsx | 14 +- .../body/renderers/formatted_field.tsx | 6 + .../timeline/body/renderers/rule_status.tsx | 27 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 27 files changed, 1321 insertions(+), 173 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index 7e1e71a01642f..c397ac313c48c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -30,6 +30,8 @@ const props = { browserFields: mockBrowserFields, eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', timelineId: 'detections-page', + title: '', + goToTable: jest.fn(), }; describe('AlertSummaryView', () => { 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 b42a0425355cc..c30837dc6eca8 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 @@ -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'; @@ -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 ( - <> - - - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 37ca3b0b897a6..14910c77d198c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -48,6 +48,8 @@ describe('EventDetails', () => { timelineId: 'test', eventView: EventsViewType.summaryView, hostRisk: { fields: [], loading: true }, + indexName: 'test', + handleOnEventClosed: jest.fn(), rawEventData, }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 0fe48d5a998ea..08f97ab7d1bc7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -6,6 +6,7 @@ */ import { + EuiHorizontalRule, EuiTabbedContent, EuiTabbedContentTab, EuiSpacer, @@ -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; @@ -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` @@ -105,18 +110,21 @@ const EventDetailsComponent: React.FC = ({ browserFields, data, id, + indexName, isAlert, isDraggable, rawEventData, timelineId, timelineTabType, hostRisk, + handleOnEventClosed, }) => { const [selectedTabId, setSelectedTabId] = useState(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( @@ -152,7 +160,19 @@ const EventDetailsComponent: React.FC = ({ name: i18n.OVERVIEW, content: ( <> + + + + = ({ timelineId, title: i18n.DUCOMENT_SUMMARY, }} + goToTable={goToTableTab} /> {(enrichmentCount > 0 || hostRisk) && ( @@ -188,8 +209,9 @@ const EventDetailsComponent: React.FC = ({ } : undefined, [ - isAlert, id, + indexName, + isAlert, data, browserFields, isDraggable, @@ -198,6 +220,8 @@ const EventDetailsComponent: React.FC = ({ allEnrichments, isEnrichmentsLoading, hostRisk, + goToTableTab, + handleOnEventClosed, ] ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index 4af444c2ab8ad..0bf404fe51e39 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -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' }, @@ -151,50 +129,34 @@ export const getSummaryRows = ({ const tableFields = getEventFieldsToDisplay({ eventCategory, eventCode }); return data != null - ? tableFields.reduce((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((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 }) => { @@ -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, @@ -234,7 +197,7 @@ export const getSummaryRows = ({ return [ ...acc, { - title: item.label ?? item.id, + title: field.label ?? field.id, description, }, ]; 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 648bc96b5c9e7..dcca42f2a1df7 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 @@ -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'; @@ -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; }; } @@ -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, + }; +} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..4e62766fc1477 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` + + .c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c6:focus-within .timelines__hoverActionButton, +.c6:focus-within .securitySolution__hoverActionButton { + opacity: 1; +} + +.c6:hover .timelines__hoverActionButton, +.c6:hover .securitySolution__hoverActionButton { + opacity: 1; +} + +.c6 .timelines__hoverActionButton, +.c6 .securitySolution__hoverActionButton { + opacity: 0; +} + +.c6 .timelines__hoverActionButton:focus, +.c6 .securitySolution__hoverActionButton:focus { + opacity: 1; +} + +.c3 { + text-transform: capitalize; +} + +.c5 { + width: 0; + -webkit-transform: translate(6px); + -ms-transform: translate(6px); + transform: translate(6px); + -webkit-transition: -webkit-transform 50ms ease-in-out; + -webkit-transition: transform 50ms ease-in-out; + transition: transform 50ms ease-in-out; + margin-left: 8px; +} + +.c1.c1.c1 { + background-color: #25262e; + padding: 8px; + height: 78px; +} + +.c1 .hoverActions-active .timelines__hoverActionButton, +.c1 .hoverActions-active .securitySolution__hoverActionButton { + opacity: 1; +} + +.c1:hover .timelines__hoverActionButton, +.c1:hover .securitySolution__hoverActionButton { + opacity: 1; +} + +.c1:hover .c4 { + width: auto; + -webkit-transform: translate(0); + -ms-transform: translate(0); + transform: translate(0); +} + +.c2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.c0 { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +
+
+
+
+
+ Status +
+
+
+
+
+
+ +
+
+
+
+
+
+

+ You are in a dialog, containing options for field kibana.alert.workflow_status. Press tab to navigate options. Press escape to exit. +

+
+ Filter button +
+
+ Filter out button +
+
+ Overflow button +
+
+
+
+
+
+
+
+
+
+ Risk Score +
+
+
+
+ 47 +
+
+
+
+

+ You are in a dialog, containing options for field kibana.alert.rule.risk_score. Press tab to navigate options. Press escape to exit. +

+
+ Filter button +
+
+ Filter out button +
+
+ Overflow button +
+
+
+
+
+
+
+
+
+
+
+
+
+ Rule +
+
+
+
+ +
+
+
+
+

+ You are in a dialog, containing options for field kibana.alert.rule.name. Press tab to navigate options. Press escape to exit. +

+
+ Filter button +
+
+ Filter out button +
+
+ Overflow button +
+
+
+
+
+
+
+
+
+ +`; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx new file mode 100644 index 0000000000000..50da80f7b1304 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { Overview } from './'; +import { TestProviders } from '../../../../common/mock'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../utils', () => ({ + useThrottledResizeObserver: () => ({ width: 400 }), // force row-chunking +})); + +describe('Event Details Overview Cards', () => { + it('renders all cards', () => { + const { getByText } = render( + + + + ); + + getByText('Status'); + getByText('Severity'); + getByText('Risk Score'); + getByText('Rule'); + }); + + it('renders all cards it has data for', () => { + const { getByText, queryByText } = render( + + + + ); + + getByText('Status'); + getByText('Risk Score'); + getByText('Rule'); + + expect(queryByText('Severity')).not.toBeInTheDocument(); + }); + + it('renders rows and spacers correctly', () => { + const { asFragment } = render( + + + + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); + +const props = { + handleOnEventClosed: jest.fn(), + contextId: 'detections-page', + eventId: 'testId', + indexName: 'testIndex', + timelineId: 'page', + data: [ + { + category: 'kibana', + field: 'kibana.alert.rule.risk_score', + values: ['47'], + originalValue: ['47'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.rule.uuid', + values: ['d9f537c0-47b2-11ec-9517-c1c68c44dec0'], + originalValue: ['d9f537c0-47b2-11ec-9517-c1c68c44dec0'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.workflow_status', + values: ['open'], + originalValue: ['open'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.rule.name', + values: ['More than one event with user name'], + originalValue: ['More than one event with user name'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.rule.severity', + values: ['medium'], + originalValue: ['medium'], + isObjectArray: false, + }, + ], + browserFields: { + kibana: { + fields: { + 'kibana.alert.rule.severity': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.severity', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.rule.risk_score': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.risk_score', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'number', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.rule.uuid': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.uuid', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.workflow_status': { + category: 'kibana', + count: 0, + name: 'kibana.alert.workflow_status', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.rule.name': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + }, + }, + }, +}; + +const dataWithoutSeverity = props.data.filter( + (data) => data.field !== 'kibana.alert.rule.severity' +); + +const fieldsWithoutSeverity = { + 'kibana.alert.rule.risk_score': props.browserFields.kibana.fields['kibana.alert.rule.risk_score'], + 'kibana.alert.rule.uuid': props.browserFields.kibana.fields['kibana.alert.rule.uuid'], + 'kibana.alert.workflow_status': props.browserFields.kibana.fields['kibana.alert.workflow_status'], + 'kibana.alert.rule.name': props.browserFields.kibana.fields['kibana.alert.rule.name'], +}; + +const propsWithoutSeverity = { + ...props, + browserFields: { kibana: { fields: fieldsWithoutSeverity } }, + data: dataWithoutSeverity, +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx new file mode 100644 index 0000000000000..70a8ec7ad0d22 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { chunk, find } from 'lodash/fp'; +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; + +import type { BrowserFields } from '../../../containers/source'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; +import type { EnrichedFieldInfo, EnrichedFieldInfoWithValues } from '../types'; +import { getEnrichedFieldInfo } from '../helpers'; +import { + ALERTS_HEADERS_RISK_SCORE, + ALERTS_HEADERS_RULE, + ALERTS_HEADERS_SEVERITY, + SIGNAL_STATUS, +} from '../../../../detections/components/alerts_table/translations'; +import { + SIGNAL_RULE_NAME_FIELD_NAME, + SIGNAL_STATUS_FIELD_NAME, +} from '../../../../timelines/components/timeline/body/renderers/constants'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import { OverviewCardWithActions } from '../overview/overview_card'; +import { StatusPopoverButton } from '../overview/status_popover_button'; +import { SeverityBadge } from '../../../../../public/detections/components/rules/severity_badge'; +import { useThrottledResizeObserver } from '../../utils'; +import { isNotNull } from '../../../../../public/timelines/store/timeline/helpers'; + +export const NotGrowingFlexGroup = euiStyled(EuiFlexGroup)` + flex-grow: 0; +`; + +interface Props { + browserFields: BrowserFields; + contextId: string; + data: TimelineEventsDetailsItem[]; + eventId: string; + handleOnEventClosed: () => void; + indexName: string; + timelineId: string; +} + +export const Overview = React.memo( + ({ browserFields, contextId, data, eventId, handleOnEventClosed, indexName, timelineId }) => { + const statusData = useMemo(() => { + const item = find({ field: SIGNAL_STATUS_FIELD_NAME, category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const severityData = useMemo(() => { + const item = find({ field: 'kibana.alert.rule.severity', category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const riskScoreData = useMemo(() => { + const item = find({ field: 'kibana.alert.rule.risk_score', category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const ruleNameData = useMemo(() => { + const item = find({ field: SIGNAL_RULE_NAME_FIELD_NAME, category: 'kibana' }, data); + const linkValueField = find({ field: 'kibana.alert.rule.uuid', category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + linkValueField, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const signalCard = hasData(statusData) ? ( + + + + + + ) : null; + + const severityCard = hasData(severityData) ? ( + + + + + + ) : null; + + const riskScoreCard = hasData(riskScoreData) ? ( + + + {riskScoreData.values[0]} + + + ) : null; + + const ruleNameCard = hasData(ruleNameData) ? ( + + + + + + ) : null; + + const { width, ref } = useThrottledResizeObserver(); + + // 675px is the container width at which none of the cards, when hovered, + // creates a visual overflow in a single row setup + const showAsSingleRow = width === 0 || width >= 675; + + // Only render cards with content + const cards = [signalCard, severityCard, riskScoreCard, ruleNameCard].filter(isNotNull); + + // If there is enough space, render a single row. + // Otherwise, render two rows with each two cards. + const content = showAsSingleRow ? ( + {cards} + ) : ( + <> + {chunk(2, cards).map((elements, index, { length }) => { + // Add a spacer between rows but not after the last row + const addSpacer = index < length - 1; + return ( + <> + {elements} + {addSpacer && } + + ); + })} + + ); + + return
{content}
; + } +); + +function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoWithValues { + return !!fieldInfo && Array.isArray(fieldInfo.values); +} + +Overview.displayName = 'Overview'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx new file mode 100644 index 0000000000000..8ed3dc7e36165 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { OverviewCardWithActions } from './overview_card'; +import { TestProviders } from '../../../../common/mock'; +import { SeverityBadge } from '../../../../../public/detections/components/rules/severity_badge'; + +const props = { + title: 'Severity', + contextId: 'timeline-case', + enrichedFieldInfo: { + contextId: 'timeline-case', + eventId: 'testid', + fieldType: 'string', + timelineId: 'timeline-case', + data: { + field: 'kibana.alert.rule.severity', + format: 'string', + type: 'string', + isObjectArray: false, + }, + values: ['medium'], + fieldFromBrowserField: { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.severity', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + description: '', + example: '', + fields: {}, + }, + }, +}; + +jest.mock('../../../lib/kibana'); + +describe('OverviewCardWithActions', () => { + test('it renders correctly', () => { + const { getByText } = render( + + + + + + ); + + // Headline + getByText('Severity'); + + // Content + getByText('Medium'); + + // Hover actions + getByText('Add To Timeline'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx new file mode 100644 index 0000000000000..4d3dae271f5c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; + +import { ActionCell } from '../table/action_cell'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { EnrichedFieldInfo } from '../types'; + +const ActionWrapper = euiStyled.div` + width: 0; + transform: translate(6px); + transition: transform 50ms ease-in-out; + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const OverviewPanel = euiStyled(EuiPanel)` + &&& { + background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + padding: ${({ theme }) => theme.eui.paddingSizes.s}; + height: 78px; + } + + & { + .hoverActions-active { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + } + + &:hover { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + + ${ActionWrapper} { + width: auto; + transform: translate(0); + } + } + } +`; + +interface OverviewCardProps { + title: string; +} + +export const OverviewCard: React.FC = ({ title, children }) => ( + + {title} + + {children} + +); + +OverviewCard.displayName = 'OverviewCard'; + +const ClampedContent = euiStyled.div` + /* Clamp text content to 2 lines */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +`; + +ClampedContent.displayName = 'ClampedContent'; + +type OverviewCardWithActionsProps = OverviewCardProps & { + contextId: string; + enrichedFieldInfo: EnrichedFieldInfo; +}; + +export const OverviewCardWithActions: React.FC = ({ + title, + children, + contextId, + enrichedFieldInfo, +}) => { + return ( + + + {children} + + + + + + + ); +}; + +OverviewCardWithActions.displayName = 'OverviewCardWithActions'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx new file mode 100644 index 0000000000000..3c3316618a72c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { StatusPopoverButton } from './status_popover_button'; +import { TestProviders } from '../../../../common/mock'; + +const props = { + eventId: 'testid', + contextId: 'detections-page', + enrichedFieldInfo: { + contextId: 'detections-page', + eventId: 'testid', + fieldType: 'string', + timelineId: 'detections-page', + data: { + field: 'kibana.alert.workflow_status', + format: 'string', + type: 'string', + isObjectArray: false, + }, + values: ['open'], + fieldFromBrowserField: { + category: 'kibana', + count: 0, + name: 'kibana.alert.workflow_status', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + description: '', + example: '', + fields: {}, + }, + }, + indexName: '.internal.alerts-security.alerts-default-000001', + timelineId: 'detections-page', + handleOnEventClosed: jest.fn(), +}; + +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', + () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), + }) +); + +describe('StatusPopoverButton', () => { + test('it renders the correct status', () => { + const { getByText } = render( + + + + ); + + getByText('open'); + }); + + test('it shows the correct options when clicked', () => { + const { getByText } = render( + + + + ); + + getByText('open').click(); + + getByText('Mark as acknowledged'); + getByText('Mark as closed'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx new file mode 100644 index 0000000000000..0ffa1570e7c29 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiContextMenuPanel, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { useAlertsActions } from '../../../../detections/components/alerts_table/timeline_actions/use_alerts_actions'; +import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + CHANGE_ALERT_STATUS, + CLICK_TO_CHANGE_ALERT_STATUS, +} from '../../../../detections/components/alerts_table/translations'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import type { EnrichedFieldInfoWithValues } from '../types'; + +interface StatusPopoverButtonProps { + eventId: string; + contextId: string; + enrichedFieldInfo: EnrichedFieldInfoWithValues; + indexName: string; + timelineId: string; + handleOnEventClosed: () => void; +} + +export const StatusPopoverButton = React.memo( + ({ eventId, contextId, enrichedFieldInfo, indexName, timelineId, handleOnEventClosed }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const closeAfterAction = useCallback(() => { + closePopover(); + handleOnEventClosed(); + }, [closePopover, handleOnEventClosed]); + + const { actionItems } = useAlertsActions({ + closePopover: closeAfterAction, + eventId, + timelineId, + indexName, + alertStatus: enrichedFieldInfo.values[0] as Status, + }); + + const button = useMemo( + () => ( + + ), + [contextId, eventId, enrichedFieldInfo, togglePopover] + ); + + return ( + + {CHANGE_ALERT_STATUS} + + + ); + } +); + +StatusPopoverButton.displayName = 'StatusPopoverButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx index d06f4d3ea105b..88208dd1b9780 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx @@ -5,18 +5,12 @@ * 2.0. */ -import { EuiTextColor, EuiFlexItem, EuiSpacer, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; -import { ALERT_REASON, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { EuiTextColor, EuiFlexItem } from '@elastic/eui'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { getRuleDetailsUrl, useFormatUrl } from '../link_to'; -import * as i18n from './translations'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { LinkAnchor } from '../links'; -import { useKibana } from '../../lib/kibana'; -import { APP_UI_ID, SecurityPageName } from '../../../../common/constants'; import { EVENT_DETAILS_PLACEHOLDER } from '../../../timelines/components/side_panel/event_details/translations'; import { getFieldValue } from '../../../detections/components/host_isolation/helpers'; @@ -25,16 +19,7 @@ interface Props { eventId: string; } -export const Indent = styled.div` - padding: 0 8px; - word-break: break-word; - line-height: 1.7em; -`; - export const ReasonComponent: React.FC = ({ eventId, data }) => { - const { navigateToApp } = useKibana().services.application; - const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const reason = useMemo(() => { const siemSignalsReason = getFieldValue( { category: 'signal', field: 'signal.alert.reason' }, @@ -44,47 +29,11 @@ export const ReasonComponent: React.FC = ({ eventId, data }) => { return aadReason.length > 0 ? aadReason : siemSignalsReason; }, [data]); - const ruleId = useMemo(() => { - const siemSignalsRuleId = getFieldValue({ category: 'signal', field: 'signal.rule.id' }, data); - const aadRuleId = getFieldValue({ category: 'kibana', field: ALERT_RULE_UUID }, data); - return aadRuleId.length > 0 ? aadRuleId : siemSignalsRuleId; - }, [data]); - if (!eventId) { return {EVENT_DETAILS_PLACEHOLDER}; } - return reason ? ( - - - -
{i18n.REASON}
-
- - - {reason} - - - - - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(ruleId), - }); - }} - href={formatUrl(getRuleDetailsUrl(ruleId))} - > - {i18n.VIEW_RULE_DETAIL_PAGE} - - - - -
- ) : null; + return reason ? {reason} : null; }; ReasonComponent.displayName = 'ReasonComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index cf8bf3ddb7474..a84d831524983 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -5,14 +5,24 @@ * 2.0. */ -import { EuiInMemoryTable, EuiBasicTableColumn, EuiTitle } from '@elastic/eui'; +import { + EuiInMemoryTable, + EuiBasicTableColumn, + EuiLink, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, +} from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { SummaryRow } from './helpers'; +import { VIEW_ALL_DOCUMENT_FIELDS } from './translations'; export const Indent = styled.div` - padding: 0 4px; + padding: 0 12px; `; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -43,18 +53,27 @@ export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` `; export const SummaryViewComponent: React.FC<{ - title?: string; + goToTable: () => void; + title: string; summaryColumns: Array>; summaryRows: SummaryRow[]; dataTestSubj?: string; -}> = ({ summaryColumns, summaryRows, dataTestSubj = 'summary-view', title }) => { +}> = ({ goToTable, summaryColumns, summaryRows, dataTestSubj = 'summary-view', title }) => { return ( - <> - {title && ( - -
{title}
-
- )} +
+ + + +
{title}
+
+
+ + + {VIEW_ALL_DOCUMENT_FIELDS} + + +
+ - +
); }; 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 74d46cf3431dc..b49aafea92245 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 @@ -8,27 +8,22 @@ import React, { useCallback, useState, useContext } from 'react'; import { HoverActions } from '../../hover_actions'; import { useActionCellDataProvider } from './use_action_cell_data_provider'; -import { EventFieldsData, FieldsData } from '../types'; +import { EnrichedFieldInfo } from '../types'; import { ColumnHeaderOptions } from '../../../../../common/types/timeline'; -import { BrowserField } from '../../../containers/source'; import { TimelineContext } from '../../../../../../timelines/public'; -interface Props { +interface Props extends EnrichedFieldInfo { contextId: string; - data: FieldsData | EventFieldsData; + applyWidthAndPadding?: boolean; disabled?: boolean; - eventId: string; - fieldFromBrowserField?: BrowserField; getLinkValue?: (field: string) => string | null; - linkValue?: string | null | undefined; onFilterAdded?: () => void; - timelineId?: string; toggleColumn?: (column: ColumnHeaderOptions) => void; - values: string[] | null | undefined; } export const ActionCell: React.FC = React.memo( ({ + applyWidthAndPadding = true, contextId, data, eventId, @@ -68,6 +63,7 @@ export const ActionCell: React.FC = React.memo( return ( ` - min-width: ${({ $hideTopN }) => `${$hideTopN ? '112px' : '138px'}`}; - padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; display: flex; ${(props) => @@ -82,8 +80,14 @@ const StyledHoverActionsContainer = styled.div<{ : ''} `; +const StyledHoverActionsContainerWithPaddingsAndMinWidth = styled(StyledHoverActionsContainer)` + min-width: ${({ $hideTopN }) => `${$hideTopN ? '112px' : '138px'}`}; + padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; +`; + interface Props { additionalContent?: React.ReactNode; + applyWidthAndPadding?: boolean; closeTopN?: () => void; closePopOver?: () => void; dataProvider?: DataProvider | DataProvider[]; @@ -128,6 +132,7 @@ export const HoverActions: React.FC = React.memo( dataType, draggableId, enableOverflowButton = false, + applyWidthAndPadding = true, field, goGetTimelineId, isObjectArray, @@ -227,6 +232,10 @@ export const HoverActions: React.FC = React.memo( values, }); + const Container = applyWidthAndPadding + ? StyledHoverActionsContainerWithPaddingsAndMinWidth + : StyledHoverActionsContainer; + return ( = React.memo( showTopN, })} > - = React.memo( {additionalContent != null && {additionalContent}} {enableOverflowButton && !isCaseView ? overflowActionItems : allActionItems} - + ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 309c6c7f9761c..1897ad45fe7ff 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -265,6 +265,20 @@ export const STATUS = i18n.translate( } ); +export const CHANGE_ALERT_STATUS = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.overview.changeAlertStatus', + { + defaultMessage: 'Change alert status', + } +); + +export const CLICK_TO_CHANGE_ALERT_STATUS = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.overview.clickToChangeAlertStatus', + { + defaultMessage: 'Click to change alert status', + } +); + export const SIGNAL_STATUS = i18n.translate( 'xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle', { @@ -278,10 +292,3 @@ export const TRIGGERED = i18n.translate( defaultMessage: 'Triggered', } ); - -export const TIMESTAMP = i18n.translate( - 'xpack.securitySolution.eventsViewer.alerts.overviewTable.timestampTitle', - { - defaultMessage: 'Timestamp', - } -); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 06dc3ea3ed967..80d8e8f9b9e26 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -439,6 +439,7 @@ exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should "indexName": "my-index", } } + handleOnEventClosed={[Function]} hostRisk={null} isAlert={false} isDraggable={false} @@ -760,6 +761,7 @@ Array [ isAlert={false} loading={true} ruleName="" + timestamp="" > void; interface Props { @@ -37,12 +40,14 @@ interface Props { timelineTabType: TimelineTabs | 'flyout'; timelineId: string; hostRisk: HostRisk | null; + handleOnEventClosed: HandleOnEventClosed; } interface ExpandableEventTitleProps { isAlert: boolean; loading: boolean; ruleName?: string; + timestamp?: string; handleOnEventClosed?: HandleOnEventClosed; } @@ -63,13 +68,22 @@ const StyledEuiFlexItem = styled(EuiFlexItem)` `; export const ExpandableEventTitle = React.memo( - ({ isAlert, loading, handleOnEventClosed, ruleName }) => ( + ({ isAlert, loading, handleOnEventClosed, ruleName, timestamp }) => ( {!loading && ( - -

{isAlert && !isEmpty(ruleName) ? ruleName : i18n.EVENT_DETAILS}

-
+ <> + +

{isAlert && !isEmpty(ruleName) ? ruleName : i18n.EVENT_DETAILS}

+
+ {timestamp && ( + <> + + + + )} + + )}
{handleOnEventClosed && ( @@ -95,6 +109,7 @@ export const ExpandableEvent = React.memo( detailsData, hostRisk, rawEventData, + handleOnEventClosed, }) => { if (!event.eventId) { return {i18n.EVENT_DETAILS_PLACEHOLDER}; @@ -112,11 +127,13 @@ export const ExpandableEvent = React.memo( data={detailsData ?? []} id={event.eventId} isAlert={isAlert} + indexName={event.indexName} isDraggable={isDraggable} rawEventData={rawEventData} timelineId={timelineId} timelineTabType={timelineTabType} hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 1d68356fc0bb7..4325e8ed64542 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -133,6 +133,11 @@ const EventDetailsPanelComponent: React.FC = ({ hostName, }); + const timestamp = useMemo( + () => getFieldValue({ category: 'base', field: '@timestamp' }, detailsData), + [detailsData] + ); + const backToAlertDetailsLink = useMemo(() => { return ( <> @@ -173,7 +178,12 @@ const EventDetailsPanelComponent: React.FC = ({ {isHostIsolationPanelOpen ? ( backToAlertDetailsLink ) : ( - + )} {isIsolateActionSuccessBannerVisible && ( @@ -203,6 +213,7 @@ const EventDetailsPanelComponent: React.FC = ({ timelineId={timelineId} timelineTabType="flyout" hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} /> )} @@ -237,6 +248,7 @@ const EventDetailsPanelComponent: React.FC = ({ timelineId={timelineId} timelineTabType={tabType} hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 63e7e164854df..ffd8da99bb607 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -57,6 +57,7 @@ const FormattedFieldValueComponent: React.FC<{ isButton?: boolean; isDraggable?: boolean; onClick?: () => void; + onClickAriaLabel?: string; title?: string; truncate?: boolean; value: string | number | undefined | null; @@ -73,6 +74,7 @@ const FormattedFieldValueComponent: React.FC<{ isObjectArray = false, isDraggable = true, onClick, + onClickAriaLabel, title, truncate = true, value, @@ -190,6 +192,10 @@ const FormattedFieldValueComponent: React.FC<{ fieldName={fieldName} isDraggable={isDraggable} value={value} + onClick={onClick} + onClickAriaLabel={onClickAriaLabel} + iconType={isButton ? 'arrowDown' : undefined} + iconSide={isButton ? 'right' : undefined} /> ); } else if (fieldName === AGENT_STATUS_FIELD_NAME) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx index d75bf436028f5..7f0d036812869 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -6,7 +6,7 @@ */ import React, { useMemo } from 'react'; -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiBadgeProps } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import styled from 'styled-components'; @@ -22,7 +22,7 @@ const StyledEuiBadge = styled(EuiBadge)` text-transform: capitalize; `; -interface Props { +interface BaseProps { contextId: string; eventId: string; fieldName: string; @@ -30,14 +30,33 @@ interface Props { value: string | number | undefined | null; } +type Props = BaseProps & + Pick; + const RuleStatusComponent: React.FC = ({ contextId, eventId, fieldName, isDraggable, value, + onClick, + onClickAriaLabel, + iconSide, + iconType, }) => { const color = useMemo(() => getOr('default', `${value}`, mapping), [value]); + const badge = ( + + {value} + + ); + return isDraggable ? ( = ({ value={`${value}`} tooltipContent={fieldName} > - {value} + {badge} ) : ( - {value} + badge ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 921794f6b6af0..b821888f0e397 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22890,7 +22890,6 @@ "xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle": "バージョン", "xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle": "ステータス", "xpack.securitySolution.eventsViewer.alerts.overviewTable.targetImportHash": "ハッシュのインポート", - "xpack.securitySolution.eventsViewer.alerts.overviewTable.timestampTitle": "タイムスタンプ", "xpack.securitySolution.eventsViewer.errorFetchingEventsData": "イベントデータをクエリできませんでした", "xpack.securitySolution.eventsViewer.eventsLabel": "イベント", "xpack.securitySolution.eventsViewer.showingLabel": "表示中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 66c2f192a6a3b..d3b1cc495da47 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23254,7 +23254,6 @@ "xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle": "版本", "xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle": "状态", "xpack.securitySolution.eventsViewer.alerts.overviewTable.targetImportHash": "导入哈希", - "xpack.securitySolution.eventsViewer.alerts.overviewTable.timestampTitle": "时间戳", "xpack.securitySolution.eventsViewer.errorFetchingEventsData": "无法查询事件数据", "xpack.securitySolution.eventsViewer.eventsLabel": "事件", "xpack.securitySolution.eventsViewer.showingLabel": "正在显示",