diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 84dfa8c1ffa1..4d044a46fd22 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -333,7 +333,7 @@ export const ELASTIC_NAME = 'estc'; export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`; -export const RISKY_HOSTS_INDEX = 'ml_host_risk_score_latest'; +export const HOST_RISK_SCORES_INDEX = 'ml_host_risk_score_latest'; export const TRANSFORM_STATES = { ABORTING: 'aborting', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index 8e65666e921f..7495e2dd2b86 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -11,7 +11,7 @@ export * from './common'; export * from './details'; export * from './first_last_seen'; export * from './kpi'; -export * from './risky_hosts'; +export * from './risk_score'; export * from './overview'; export * from './uncommon_processes'; @@ -23,6 +23,6 @@ export enum HostsQueries { hosts = 'hosts', hostsEntities = 'hostsEntities', overview = 'overviewHost', - riskyHosts = 'riskyHosts', + hostsRiskScore = 'hostsRiskScore', uncommonProcesses = 'uncommonProcesses', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts new file mode 100644 index 000000000000..39f648eab8cd --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { FactoryQueryTypes } from '../..'; +import { + IEsSearchRequest, + IEsSearchResponse, +} from '../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe, TimerangeInput } from '../../../common'; + +export interface HostsRiskScoreRequestOptions extends IEsSearchRequest { + defaultIndex: string[]; + factoryQueryType?: FactoryQueryTypes; + hostName?: string; + timerange?: TimerangeInput; +} + +export interface HostsRiskScoreStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; +} + +export interface HostsRiskScore { + host: { + name: string; + }; + risk_score: number; + risk: string; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risky_hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risky_hosts/index.ts deleted file mode 100644 index f6290e5321a3..000000000000 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risky_hosts/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 { Inspect, Maybe, RequestBasicOptions } from '../../..'; -import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; - -export type HostsRiskyHostsRequestOptions = RequestBasicOptions; - -export interface HostsRiskyHostsStrategyResponse extends IEsSearchResponse { - inspect?: Maybe; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 47a96d8a5fe6..9a176662fe86 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -28,8 +28,8 @@ import { HostsKpiUniqueIpsStrategyResponse, HostsKpiUniqueIpsRequestOptions, HostFirstLastSeenRequestOptions, - HostsRiskyHostsStrategyResponse, - HostsRiskyHostsRequestOptions, + HostsRiskScoreStrategyResponse, + HostsRiskScoreRequestOptions, } from './hosts'; import { NetworkQueries, @@ -126,8 +126,8 @@ export type StrategyResponseType = T extends HostsQ ? HostDetailsStrategyResponse : T extends UebaQueries.riskScore ? RiskScoreStrategyResponse - : T extends HostsQueries.riskyHosts - ? HostsRiskyHostsStrategyResponse + : T extends HostsQueries.hostsRiskScore + ? HostsRiskScoreStrategyResponse : T extends UebaQueries.hostRules ? HostRulesStrategyResponse : T extends UebaQueries.userRules @@ -182,8 +182,8 @@ export type StrategyResponseType = T extends HostsQ export type StrategyRequestType = T extends HostsQueries.hosts ? HostsRequestOptions - : T extends HostsQueries.riskyHosts - ? HostsRiskyHostsRequestOptions + : T extends HostsQueries.hostsRiskScore + ? HostsRiskScoreRequestOptions : T extends HostsQueries.details ? HostDetailsRequestOptions : T extends HostsQueries.overview diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap index f11150908375..2904d8184261 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap @@ -1,10 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AlertSummaryView Behavior event code renders additional summary rows 1`] = ` -.c1 { - line-height: 1.7rem; -} - .c0 .euiTableHeaderCell, .c0 .euiTableRowCell { border: none; @@ -24,6 +20,10 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1` opacity: 1; } +.c1 { + line-height: 1.7rem; +} + .c2 { min-width: 138px; padding: 0 8px; @@ -53,10 +53,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1` opacity: 1; } -.c3 { - padding: 4px 0; -} -
-
+
@@ -205,7 +203,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -273,7 +273,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -337,7 +339,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -401,7 +405,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -465,7 +471,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -529,7 +537,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -593,7 +603,9 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
@@ -644,161 +656,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
- - -
-
- destination.ip -
-
- - -
-
- — -
-
- - - - -
-
- Threshold Count -
-
- - -
-
- — -
-
- - - - -
-
- Threshold Terms -
-
- - -
-
- — -
-
- - - - -
-
- Threshold Cardinality -
-
- - -
-
- — -
-
- - - - -
-
- Rule description -
-
- - -
-
- — -
-
- -
@@ -806,10 +663,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1` `; exports[`AlertSummaryView Memory event code renders additional summary rows 1`] = ` -.c1 { - line-height: 1.7rem; -} - .c0 .euiTableHeaderCell, .c0 .euiTableRowCell { border: none; @@ -829,6 +682,10 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`] opacity: 1; } +.c1 { + line-height: 1.7rem; +} + .c2 { min-width: 138px; padding: 0 8px; @@ -858,10 +715,6 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`] opacity: 1; } -.c3 { - padding: 4px 0; -} -
-
+
@@ -1010,7 +865,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1078,7 +935,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1142,7 +1001,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1206,7 +1067,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1270,7 +1133,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1334,7 +1199,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1398,7 +1265,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
-
+
@@ -1449,192 +1318,6 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
- - -
-
- destination.ip -
-
- - -
-
- — -
-
- - - - -
-
- Threshold Count -
-
- - -
-
- — -
-
- - - - -
-
- Threshold Terms -
-
- - -
-
- — -
-
- - - - -
-
- Threshold Cardinality -
-
- - -
-
- — -
-
- - - - -
-
- Rule name -
-
- - -
-
- — -
-
- - - - -
-
- Import Hash -
-
- - -
-
- — -
-
- -
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 2b399a057117..fcc943f56589 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 @@ -6,7 +6,7 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { waitFor, render } from '@testing-library/react'; import { AlertSummaryView } from './alert_summary_view'; import { mockAlertDetailsData } from './__mocks__'; @@ -15,7 +15,6 @@ import { useRuleWithFallback } from '../../../detections/containers/detection_en import { TestProviders, TestProvidersComponent } from '../../mock'; import { mockBrowserFields } from '../../containers/source/mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/kibana'); @@ -33,8 +32,6 @@ const props = { }; describe('AlertSummaryView', () => { - const mount = useMountAppended(); - beforeEach(() => { jest.clearAllMocks(); (useRuleWithFallback as jest.Mock).mockReturnValue({ @@ -44,23 +41,12 @@ describe('AlertSummaryView', () => { }); }); test('render correct items', () => { - const wrapper = mount( + const { getByTestId } = render( ); - expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true); - }); - - test('render investigation guide', async () => { - const wrapper = mount( - - - - ); - await waitFor(() => { - expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true); - }); + expect(getByTestId('summary-view')).toBeInTheDocument(); }); test("render no investigation guide if it doesn't exist", async () => { @@ -69,13 +55,13 @@ describe('AlertSummaryView', () => { note: null, }, }); - const wrapper = mount( + const { queryByTestId } = render( ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(false); + expect(queryByTestId('summary-view-guide')).not.toBeInTheDocument(); }); }); test('Memory event code renders additional summary rows', () => { @@ -93,12 +79,12 @@ describe('AlertSummaryView', () => { return item; }) as TimelineEventsDetailsItem[], }; - const wrapper = mount( + const { container } = render( ); - expect(wrapper.find('div[data-test-subj="summary-view"]').render()).toMatchSnapshot(); + expect(container.querySelector('div[data-test-subj="summary-view"]')).toMatchSnapshot(); }); test('Behavior event code renders additional summary rows', () => { const renderProps = { @@ -115,11 +101,36 @@ describe('AlertSummaryView', () => { return item; }) as TimelineEventsDetailsItem[], }; - const wrapper = mount( + const { container } = render( ); - expect(wrapper.find('div[data-test-subj="summary-view"]').render()).toMatchSnapshot(); + expect(container.querySelector('div[data-test-subj="summary-view"]')).toMatchSnapshot(); + }); + + test("doesn't render empty fields", () => { + const renderProps = { + ...props, + data: mockAlertDetailsData.map((item) => { + if (item.category === 'signal' && item.field === 'signal.rule.name') { + return { + category: 'signal', + field: 'signal.rule.name', + values: undefined, + originalValue: undefined, + }; + } + return item; + }) as TimelineEventsDetailsItem[], + }; + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('event-field-signal.rule.name')).not.toBeInTheDocument(); }); }); 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 e7816fd1daaa..19a23e500256 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,113 +5,18 @@ * 2.0. */ -import { EuiBasicTableColumn, EuiSpacer, EuiHorizontalRule, EuiTitle, EuiText } from '@elastic/eui'; -import { get, getOr, find, isEmpty } from 'lodash/fp'; +import { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; -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 { SummaryView } from './summary_view'; import { AlertSummaryRow, getSummaryColumns, SummaryRow } from './helpers'; -import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback'; -import { MarkdownRenderer } from '../markdown_editor'; -import { LineClamp } from '../line_clamp'; -import { isAlertFromEndpointEvent } 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 { EventCode } from '../../../../common/ecs/event'; -export const Indent = styled.div` - padding: 0 8px; - word-break: break-word; -`; - -const StyledEmptyComponent = styled.div` - padding: ${(props) => `${props.theme.eui.paddingSizes.xs} 0`}; -`; - -interface EventSummaryField { - id: string; - label?: string; - linkField?: string; - fieldType?: string; - overrideField?: string; -} - -const defaultDisplayFields: EventSummaryField[] = [ - { id: 'signal.status', label: SIGNAL_STATUS }, - { id: '@timestamp', label: TIMESTAMP }, - { - id: SIGNAL_RULE_NAME_FIELD_NAME, - linkField: 'signal.rule.id', - label: ALERTS_HEADERS_RULE, - }, - { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, - { id: 'signal.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' }, - { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, - { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, - { id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT }, - { id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS }, - { id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY }, -]; - -const processCategoryFields: EventSummaryField[] = [ - ...defaultDisplayFields, - { id: 'process.name' }, - { id: 'process.parent.name' }, - { id: 'process.args' }, -]; - -const networkCategoryFields: EventSummaryField[] = [ - ...defaultDisplayFields, - { id: 'destination.address' }, - { id: 'destination.port' }, - { id: 'process.name' }, -]; - -const memoryShellCodeAlertFields: EventSummaryField[] = [ - ...defaultDisplayFields, - { id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME }, - { - id: 'Target.process.thread.Ext.start_address_details.memory_pe.imphash', - label: ALERTS_HEADERS_TARGET_IMPORT_HASH, - }, -]; - -const behaviorAlertFields: EventSummaryField[] = [ - ...defaultDisplayFields, - { id: 'rule.description', label: ALERTS_HEADERS_RULE_DESCRIPTION }, -]; - -const memorySignatureAlertFields: EventSummaryField[] = [ - ...defaultDisplayFields, - { id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME }, -]; +import { getSummaryRows } from './get_alert_summary_rows'; const getDescription = ({ data, @@ -121,187 +26,28 @@ const getDescription = ({ linkValue, timelineId, values, -}: AlertSummaryRow['description']) => { - if (isEmpty(values)) { - return {getEmptyValue()}; - } - - return ( - <> - - - - ); -}; - -function getEventFieldsToDisplay({ - eventCategory, - eventCode, -}: { - eventCategory: string; - eventCode?: string; -}): EventSummaryField[] { - switch (eventCode) { - // memory protection fields - case EventCode.SHELLCODE_THREAD: - return memoryShellCodeAlertFields; - case EventCode.MEMORY_SIGNATURE: - return memorySignatureAlertFields; - case EventCode.BEHAVIOR: - return behaviorAlertFields; - } - - switch (eventCategory) { - case 'network': - return networkCategoryFields; - - case 'process': - return processCategoryFields; - } - - return defaultDisplayFields; -} - -export const getSummaryRows = ({ - data, - browserFields, - timelineId, - eventId, - isDraggable = false, -}: { - data: TimelineEventsDetailsItem[]; - browserFields: BrowserFields; - timelineId: string; - eventId: string; - isDraggable?: boolean; -}) => { - const eventCategoryField = find({ category: 'event', field: 'event.category' }, data); - - const eventCategory = Array.isArray(eventCategoryField?.originalValue) - ? eventCategoryField?.originalValue[0] - : eventCategoryField?.originalValue; - - const eventCodeField = find({ category: 'event', field: 'event.code' }, data); - - const eventCode = Array.isArray(eventCodeField?.originalValue) - ? eventCodeField?.originalValue?.[0] - : eventCodeField?.originalValue; - - 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) { - 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 fieldName = field.field ?? ''; - - const browserField = get([category, 'fields', fieldName], browserFields); - 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, - }; - - if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) { - return acc; - } - - if (item.id === 'signal.threshold_result.terms') { - try { - const terms = getOr(null, 'originalValue', field); - const parsedValue = terms.map((term: string) => JSON.parse(term)); - const thresholdTerms = (parsedValue ?? []).map( - (entry: { field: string; value: string }) => { - return { - title: `${entry.field} [threshold]`, - description: { - ...description, - values: [entry.value], - }, - }; - } - ); - return [...acc, ...thresholdTerms]; - } catch (err) { - return [...acc]; - } - } - - if (item.id === 'signal.threshold_result.cardinality') { - try { - const parsedValue = JSON.parse(value); - return [ - ...acc, - { - title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, - description: { - ...description, - values: [`count(${parsedValue.field}) == ${parsedValue.value}`], - }, - }, - ]; - } catch (err) { - return acc; - } - } - - return [ - ...acc, - { - title: item.label ?? item.id, - description, - }, - ]; - }, []) - : []; -}; +}: AlertSummaryRow['description']) => ( + <> + + + +); const summaryColumns: Array> = getSummaryColumns(getDescription); @@ -318,33 +64,10 @@ const AlertSummaryViewComponent: React.FC<{ [browserFields, data, eventId, isDraggable, timelineId] ); - const ruleId = useMemo(() => { - const item = data.find((d) => d.field === 'signal.rule.id'); - return Array.isArray(item?.originalValue) - ? item?.originalValue[0] - : item?.originalValue ?? null; - }, [data]); - const { rule: maybeRule } = useRuleWithFallback(ruleId); - return ( <> + - {maybeRule?.note && ( - <> - - -
{i18n.INVESTIGATION_GUIDE}
-
- - - - - {maybeRule.note} - - - - - )} ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx index e1e35b0d2c5c..dd242884e5d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_icon.tsx @@ -13,8 +13,8 @@ import { isInvestigationTimeEnrichment } from './helpers'; export const getTooltipTitle = (type: string | undefined) => isInvestigationTimeEnrichment(type) - ? i18n.INVESTIGATION_TOOLTIP_TITLE - : i18n.INDICATOR_TOOLTIP_TITLE; + ? i18n.INVESTIGATION_ENRICHMENT_TITLE + : i18n.INDICATOR_ENRICHMENT_TITLE; export const getTooltipContent = (type: string | undefined) => isInvestigationTimeEnrichment(type) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx new file mode 100644 index 000000000000..37fbab924afa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx @@ -0,0 +1,189 @@ +/* + * 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 styled from 'styled-components'; +import { get } from 'lodash/fp'; +import React from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { partition } from 'lodash'; +import * as i18n from './translations'; +import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; +import { getEnrichmentIdentifiers, isInvestigationTimeEnrichment } from './helpers'; + +import { FieldsData } from '../types'; +import { ActionCell } from '../table/action_cell'; +import { BrowserField, BrowserFields, TimelineEventsDetailsItem } from '../../../../../common'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view'; + +export interface ThreatSummaryDescription { + browserField: BrowserField; + data: FieldsData | undefined; + eventId: string; + index: number; + provider: string | undefined; + timelineId: string; + value: string | undefined; + isDraggable?: boolean; +} + +const EnrichmentFieldProvider = styled.span` + margin-left: ${({ theme }) => theme.eui.paddingSizes.xs}; + white-space: nowrap; + font-style: italic; +`; + +const EnrichmentDescription: React.FC = ({ + browserField, + data, + eventId, + index, + provider, + timelineId, + value, + isDraggable, +}) => { + if (!data || !value) return null; + const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${data.field}-${value}-${index}-${provider}`; + return ( + + +
+ + {provider && ( + + {i18n.PROVIDER_PREPOSITION} {provider} + + )} +
+
+ + {value && ( + + )} + +
+ ); +}; + +const EnrichmentSummaryComponent: React.FC<{ + browserFields: BrowserFields; + data: TimelineEventsDetailsItem[]; + enrichments: CtiEnrichment[]; + timelineId: string; + eventId: string; + isDraggable?: boolean; +}> = ({ browserFields, data, enrichments, timelineId, eventId, isDraggable }) => { + const parsedEnrichments = enrichments.map((enrichment, index) => { + const { field, type, provider, value } = getEnrichmentIdentifiers(enrichment); + const eventData = data.find((item) => item.field === field); + const category = eventData?.category ?? ''; + const browserField = get([category, 'fields', field ?? ''], browserFields); + + const fieldsData: FieldsData = { + field: field ?? '', + format: browserField?.format ?? '', + type: browserField?.type ?? '', + isObjectArray: eventData?.isObjectArray ?? false, + }; + + return { + fieldsData, + type, + provider, + index, + field, + browserField, + value, + }; + }); + + const [investigation, indicator] = partition(parsedEnrichments, ({ type }) => + isInvestigationTimeEnrichment(type) + ); + + return ( + <> + {indicator.length > 0 && ( + + + + + {indicator.map(({ fieldsData, index, field, provider, browserField, value }) => ( + + } + /> + ))} + + + )} + + {investigation.length > 0 && ( + + + + + {investigation.map(({ fieldsData, index, field, provider, browserField, value }) => ( + + } + /> + ))} + + + )} + + ); +}; +export const EnrichmentSummary = React.memo(EnrichmentSummaryComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx new file mode 100644 index 000000000000..21b86fc1740b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx @@ -0,0 +1,96 @@ +/* + * 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 { TestProviders } from '../../../mock'; +import { NO_HOST_RISK_DATA_DESCRIPTION } from './translations'; +import { HostRiskSummary } from './host_risk_summary'; + +describe('HostRiskSummary', () => { + it('renders host risk data', () => { + const riskKeyword = 'test risk'; + const hostRisk = { + loading: false, + isModuleEnabled: true, + result: [ + { + host: { + name: 'test-host-name', + }, + risk_score: 9999, + risk: riskKeyword, + }, + ], + }; + + const { getByText } = render( + + + + ); + + expect(getByText(riskKeyword)).toBeInTheDocument(); + }); + + it('renders spinner when loading', () => { + const hostRisk = { + loading: true, + isModuleEnabled: true, + result: [], + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('loading')).toBeInTheDocument(); + }); + + it('renders no host data message when module is diabled', () => { + const hostRisk = { + loading: false, + isModuleEnabled: false, + result: [ + { + host: { + name: 'test-host-name', + }, + risk_score: 9999, + risk: 'test-risk', + }, + ], + }; + + const { getByText } = render( + + + + ); + + expect(getByText(NO_HOST_RISK_DATA_DESCRIPTION)).toBeInTheDocument(); + }); + + it('renders no host data message when there is no host data', () => { + const hostRisk = { + loading: false, + isModuleEnabled: true, + result: [], + }; + + const { getByText } = render( + + + + ); + + expect(getByText(NO_HOST_RISK_DATA_DESCRIPTION)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx new file mode 100644 index 000000000000..425bba4f19f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx @@ -0,0 +1,61 @@ +/* + * 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 { EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import * as i18n from './translations'; +import { RISKY_HOSTS_DOC_LINK } from '../../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; +import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view'; + +const HostRiskSummaryComponent: React.FC<{ + hostRisk: HostRisk; +}> = ({ hostRisk }) => ( + <> + + + + + ), + }} + /> + } + /> + + {hostRisk.loading && } + + {!hostRisk.loading && (!hostRisk.isModuleEnabled || hostRisk.result?.length === 0) && ( + <> + + + {i18n.NO_HOST_RISK_DATA_DESCRIPTION} + + + )} + + {hostRisk.isModuleEnabled && hostRisk.result && hostRisk.result.length > 0 && ( + <> + + + )} + + +); + +export const HostRiskSummary = React.memo(HostRiskSummaryComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index 949d55c7aff5..5800ffb4beb9 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -30,8 +30,8 @@ const EnrichmentSectionHeader: React.FC<{ type?: ENRICHMENT_TYPES }> = ({ type }
{type === ENRICHMENT_TYPES.IndicatorMatchRule - ? i18n.INDICATOR_TOOLTIP_TITLE - : i18n.INVESTIGATION_TOOLTIP_TITLE} + ? i18n.INDICATOR_ENRICHMENT_TITLE + : i18n.INVESTIGATION_ENRICHMENT_TITLE}
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx index fe85fd573cfa..3986b37656ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx @@ -9,28 +9,36 @@ import React from 'react'; import { ThreatSummaryView } from './threat_summary_view'; import { TestProviders } from '../../../mock'; -import { useMountAppended } from '../../../utils/use_mount_appended'; +import { render } from '@testing-library/react'; import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; import { mockAlertDetailsData } from '../__mocks__'; import { TimelineEventsDetailsItem } from '../../../../../../timelines/common'; import { mockBrowserFields } from '../../../containers/source/mock'; +import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; + +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: () => ({ + services: { + timelines: { ...mockTimelines }, + }, + }), +})); jest.mock('../table/action_cell'); jest.mock('../table/field_name_cell'); describe('ThreatSummaryView', () => { - const mount = useMountAppended(); const eventId = '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'; const timelineId = 'detections-page'; const data = mockAlertDetailsData as TimelineEventsDetailsItem[]; const browserFields = mockBrowserFields; - it('renders a row for each enrichment', () => { + it("renders 'Enriched with Threat Intelligence' panel with fields", () => { const enrichments = [ buildEventEnrichmentMock({ 'matched.id': ['test.id'], 'matched.field': ['test.field'] }), buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), ]; - const wrapper = mount( + const { getByText, getAllByTestId } = render( { enrichments={enrichments} eventId={eventId} timelineId={timelineId} + hostRisk={null} /> ); - expect(wrapper.find('[data-test-subj="threat-summary-view"] .euiTableRow')).toHaveLength( - enrichments.length - ); + expect(getByText('Enriched with Threat Intelligence')).toBeInTheDocument(); + + expect(getAllByTestId('EnrichedDataRow')).toHaveLength(enrichments.length); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index 0756fc8dad88..bdd342934eeb 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -6,172 +6,169 @@ */ import styled from 'styled-components'; -import { get } from 'lodash/fp'; -import React, { Fragment } from 'react'; -import { EuiBasicTableColumn, EuiText, EuiTitle } from '@elastic/eui'; - +import React, { useCallback, useState } from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiPopover, + EuiButtonIcon, + EuiPopoverTitle, + EuiText, +} from '@elastic/eui'; import * as i18n from './translations'; -import { Indent, StyledEuiInMemoryTable } from '../summary_view'; import { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { getEnrichmentIdentifiers } from './helpers'; -import { EnrichmentIcon } from './enrichment_icon'; -import { FieldsData } from '../types'; -import { ActionCell } from '../table/action_cell'; -import { BrowserField, BrowserFields, TimelineEventsDetailsItem } from '../../../../../common'; -import { FieldValueCell } from '../table/field_value_cell'; -export interface ThreatSummaryItem { - title: { - title: string | undefined; - type: string | undefined; - }; - description: { - browserField: BrowserField; - data: FieldsData | undefined; - eventId: string; - index: number; - provider: string | undefined; - timelineId: string; - value: string | undefined; - }; +import { FieldsData } from '../types'; + +import { BrowserField, BrowserFields, TimelineEventsDetailsItem } from '../../../../../common'; +import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import { HostRiskSummary } from './host_risk_summary'; +import { EnrichmentSummary } from './enrichment_summary'; + +export interface ThreatSummaryDescription { + browserField: BrowserField; + data: FieldsData | undefined; + eventId: string; + index: number; + provider: string | undefined; + timelineId: string; + value: string | undefined; + isDraggable?: boolean; } -const RightMargin = styled.span` - margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; - min-width: 30px; +const UppercaseEuiTitle = styled(EuiTitle)` + text-transform: uppercase; `; -const EnrichmentTitle: React.FC = ({ title, type }) => ( - <> - - -
{title}
-
-
- - +const ThreatSummaryPanelTitle: React.FC = ({ children }) => ( + +
{children}
+
); -const EnrichmentDescription: React.FC = ({ - browserField, - data, - eventId, - index, - provider, - timelineId, +const StyledEnrichmentFieldTitle = styled(EuiTitle)` + width: 220px; +`; + +const EnrichmentFieldTitle: React.FC<{ + title: string | undefined; +}> = ({ title }) => ( + +
{title}
+
+); + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + margin-top: ${({ theme }) => theme.eui.euiSizeS}; +`; + +export const EnrichedDataRow: React.FC<{ field: string | undefined; value: React.ReactNode }> = ({ + field, value, -}) => { - if (!data || !value) return null; - const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${data.field}-${value}-${index}-${provider}`; +}) => ( + + + + + {value} + +); + +export const ThreatSummaryPanelHeader: React.FC<{ + title: string; + toolTipContent: React.ReactNode; +}> = ({ title, toolTipContent }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onClick = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen, setIsPopoverOpen]); + + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, [setIsPopoverOpen]); + return ( - - - - - {provider && ( - <> - - - {i18n.PROVIDER_PREPOSITION} - - - - - {provider} - - - - )} - {value && ( - - )} - + + + {title} + + + + } + > + {title} + + {toolTipContent} + + + + ); }; -const buildThreatSummaryItems = ( - browserFields: BrowserFields, - data: TimelineEventsDetailsItem[], - enrichments: CtiEnrichment[], - timelineId: string, - eventId: string -) => { - return enrichments.map((enrichment, index) => { - const { field, type, value, provider } = getEnrichmentIdentifiers(enrichment); - const eventData = data.find((item) => item.field === field); - const category = eventData?.category ?? ''; - const browserField = get([category, 'fields', field ?? ''], browserFields); - - const fieldsData = { - field, - format: browserField?.format ?? '', - type: browserField?.type ?? '', - isObjectArray: eventData?.isObjectArray, - }; - - return { - title: { - title: field, - type, - }, - description: { - eventId, - index, - provider, - timelineId, - value, - data: fieldsData, - browserField, - }, - }; - }); -}; - -const columns: Array> = [ - { - field: 'title', - truncateText: false, - render: EnrichmentTitle, - width: '220px', - name: '', - }, - { - className: 'flyoutOverviewDescription', - field: 'description', - truncateText: false, - render: EnrichmentDescription, - name: '', - }, -]; - const ThreatSummaryViewComponent: React.FC<{ browserFields: BrowserFields; data: TimelineEventsDetailsItem[]; enrichments: CtiEnrichment[]; eventId: string; timelineId: string; -}> = ({ browserFields, data, enrichments, eventId, timelineId }) => ( - - - -); + hostRisk: HostRisk | null; + isDraggable?: boolean; +}> = ({ browserFields, data, enrichments, eventId, timelineId, hostRisk, isDraggable }) => { + if (!hostRisk && enrichments.length === 0) { + return null; + } + + return ( + <> + + + +
{i18n.ENRICHED_DATA}
+
+ + + + {hostRisk && ( + + + + )} + + + + + ); +}; export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts index 64775f30b7f3..14a1fde29d15 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -14,20 +14,27 @@ export const PROVIDER_PREPOSITION = i18n.translate( } ); -export const INDICATOR_TOOLTIP_TITLE = i18n.translate( - 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle', +export const INDICATOR_ENRICHMENT_TITLE = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTitle', { defaultMessage: 'Threat Match Detected', } ); -export const INVESTIGATION_TOOLTIP_TITLE = i18n.translate( - 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle', +export const INVESTIGATION_ENRICHMENT_TITLE = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle', { defaultMessage: 'Enriched with Threat Intelligence', } ); +export const HOST_RISK_DATA_TITLE = i18n.translate( + 'xpack.securitySolution.alertDetails.overview.hostRiskDataTitle', + { + defaultMessage: 'Host Risk Data', + } +); + export const INDICATOR_TOOLTIP_CONTENT = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent', { @@ -36,6 +43,13 @@ export const INDICATOR_TOOLTIP_CONTENT = i18n.translate( } ); +export const INFORMATION_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.informationAriaLabel', + { + defaultMessage: 'Information', + } +); + export const INVESTIGATION_TOOLTIP_CONTENT = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent', { @@ -82,6 +96,13 @@ export const NO_ENRICHMENTS_FOUND_DESCRIPTION = i18n.translate( } ); +export const NO_HOST_RISK_DATA_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.alertDetails.noRiskDataDescription', + { + defaultMessage: 'These is no host risk data found for this alert', + } +); + export const CHECK_DOCS = i18n.translate('xpack.securitySolution.alertDetails.checkDocs', { defaultMessage: 'please check out our documentation', }); @@ -117,3 +138,10 @@ export const ENRICHMENT_LOOKBACK_END_DATE = i18n.translate( export const REFRESH = i18n.translate('xpack.securitySolution.alertDetails.refresh', { defaultMessage: 'Refresh', }); + +export const ENRICHED_DATA = i18n.translate( + 'xpack.securitySolution.alertDetails.overview.enrichedDataTitle', + { + defaultMessage: 'Enriched data', + } +); 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 47d407413316..a8ba536a7554 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 @@ -24,6 +24,16 @@ import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enric jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/cti/event_enrichment'); +jest.mock('../../../detections/containers/detection_engine/rules/use_rule_with_fallback', () => { + return { + useRuleWithFallback: jest.fn().mockReturnValue({ + rule: { + note: 'investigation guide', + }, + }), + }; +}); + jest.mock('../link_to'); describe('EventDetails', () => { const mount = useMountAppended(); @@ -37,6 +47,7 @@ describe('EventDetails', () => { timelineTabType: TimelineTabs.query, timelineId: 'test', eventView: EventsViewType.summaryView, + hostRisk: { fields: [], loading: true }, }; const alertsProps = { @@ -115,6 +126,12 @@ describe('EventDetails', () => { }); }); + describe('summary view tab', () => { + it('render investigation guide', () => { + expect(alertsWrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true); + }); + }); + describe('threat intel tab', () => { it('renders a "no enrichments" panel view if there are no enrichments', () => { alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click'); 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 3c96f6746a50..e7092d9d6f46 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 @@ -10,10 +10,10 @@ import { EuiTabbedContentTab, EuiSpacer, EuiLoadingContent, - EuiLoadingSpinner, EuiNotificationBadge, EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -38,6 +38,9 @@ import { import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker'; import { Reason } from './reason'; +import { InvestigationGuideView } from './investigation_guide_view'; +import { HostRisk } from '../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; + type EventViewTab = EuiTabbedContentTab; export type EventViewId = @@ -60,8 +63,14 @@ interface Props { isDraggable?: boolean; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; + hostRisk: HostRisk | null; } +export const Indent = styled.div` + padding: 0 8px; + word-break: break-word; +`; + const StyledEuiTabbedContent = styled(EuiTabbedContent)` display: flex; flex: 1; @@ -99,6 +108,7 @@ const EventDetailsComponent: React.FC = ({ isDraggable, timelineId, timelineTabType, + hostRisk, }) => { const [selectedTabId, setSelectedTabId] = useState(EventsViewType.summaryView); const handleTabClick = useCallback( @@ -151,8 +161,11 @@ const EventDetailsComponent: React.FC = ({ title: i18n.DUCOMENT_SUMMARY, }} /> - {enrichmentCount > 0 && ( + + {(enrichmentCount > 0 || hostRisk) && ( = ({ enrichments={allEnrichments} /> )} + {isEnrichmentsLoading && ( <> )} + + ), } @@ -179,6 +195,7 @@ const EventDetailsComponent: React.FC = ({ enrichmentCount, allEnrichments, isEnrichmentsLoading, + hostRisk, ] ); 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 new file mode 100644 index 000000000000..52d31e348459 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -0,0 +1,243 @@ +/* + * 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 { get, 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 { 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: 'signal.status', label: SIGNAL_STATUS }, + { id: '@timestamp', label: TIMESTAMP }, + { + id: SIGNAL_RULE_NAME_FIELD_NAME, + linkField: 'signal.rule.id', + label: ALERTS_HEADERS_RULE, + }, + { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, + { id: 'signal.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' }, + { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: 'signal.threshold_result.count', label: ALERTS_HEADERS_THRESHOLD_COUNT }, + { id: 'signal.threshold_result.terms', label: ALERTS_HEADERS_THRESHOLD_TERMS }, + { id: 'signal.threshold_result.cardinality', label: ALERTS_HEADERS_THRESHOLD_CARDINALITY }, +]; + +const processCategoryFields: EventSummaryField[] = [ + ...defaultDisplayFields, + { id: 'process.name' }, + { id: 'process.parent.name' }, + { id: 'process.args' }, +]; + +const networkCategoryFields: EventSummaryField[] = [ + ...defaultDisplayFields, + { id: 'destination.address' }, + { id: 'destination.port' }, + { id: 'process.name' }, +]; + +const memoryShellCodeAlertFields: EventSummaryField[] = [ + ...defaultDisplayFields, + { id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME }, + { + id: 'Target.process.thread.Ext.start_address_details.memory_pe.imphash', + label: ALERTS_HEADERS_TARGET_IMPORT_HASH, + }, +]; + +const behaviorAlertFields: EventSummaryField[] = [ + ...defaultDisplayFields, + { id: 'rule.description', label: ALERTS_HEADERS_RULE_DESCRIPTION }, +]; + +const memorySignatureAlertFields: EventSummaryField[] = [ + ...defaultDisplayFields, + { id: 'rule.name', label: ALERTS_HEADERS_RULE_NAME }, +]; + +function getEventFieldsToDisplay({ + eventCategory, + eventCode, +}: { + eventCategory: string; + eventCode?: string; +}): EventSummaryField[] { + switch (eventCode) { + // memory protection fields + case EventCode.SHELLCODE_THREAD: + return memoryShellCodeAlertFields; + case EventCode.MEMORY_SIGNATURE: + return memorySignatureAlertFields; + case EventCode.BEHAVIOR: + return behaviorAlertFields; + } + + switch (eventCategory) { + case 'network': + return networkCategoryFields; + + case 'process': + return processCategoryFields; + } + + return defaultDisplayFields; +} + +export const getSummaryRows = ({ + data, + browserFields, + timelineId, + eventId, + isDraggable = false, +}: { + data: TimelineEventsDetailsItem[]; + browserFields: BrowserFields; + timelineId: string; + eventId: string; + isDraggable?: boolean; +}) => { + const eventCategoryField = find({ category: 'event', field: 'event.category' }, data); + + const eventCategory = Array.isArray(eventCategoryField?.originalValue) + ? eventCategoryField?.originalValue[0] + : eventCategoryField?.originalValue; + + const eventCodeField = find({ category: 'event', field: 'event.code' }, data); + + const eventCode = Array.isArray(eventCodeField?.originalValue) + ? eventCodeField?.originalValue?.[0] + : eventCodeField?.originalValue; + + 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)) { + 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); + 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, + }; + + if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) { + return acc; + } + + if (item.id === 'signal.threshold_result.terms') { + try { + const terms = getOr(null, 'originalValue', field); + const parsedValue = terms.map((term: string) => JSON.parse(term)); + const thresholdTerms = (parsedValue ?? []).map( + (entry: { field: string; value: string }) => { + return { + title: `${entry.field} [threshold]`, + description: { + ...description, + values: [entry.value], + }, + }; + } + ); + return [...acc, ...thresholdTerms]; + } catch (err) { + return [...acc]; + } + } + + if (item.id === 'signal.threshold_result.cardinality') { + try { + const parsedValue = JSON.parse(value); + return [ + ...acc, + { + title: ALERTS_HEADERS_THRESHOLD_CARDINALITY, + description: { + ...description, + values: [`count(${parsedValue.field}) == ${parsedValue.value}`], + }, + }, + ]; + } catch (err) { + return acc; + } + } + + return [ + ...acc, + { + title: item.label ?? item.id, + description, + }, + ]; + }, []) + : []; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx new file mode 100644 index 000000000000..313766caad19 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -0,0 +1,57 @@ +/* + * 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 { EuiSpacer, EuiHorizontalRule, EuiTitle, EuiText } from '@elastic/eui'; + +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { useRuleWithFallback } from '../../../detections/containers/detection_engine/rules/use_rule_with_fallback'; +import { MarkdownRenderer } from '../markdown_editor'; +import { LineClamp } from '../line_clamp'; +import { TimelineEventsDetailsItem } from '../../../../common'; + +export const Indent = styled.div` + padding: 0 8px; + word-break: break-word; +`; + +const InvestigationGuideViewComponent: React.FC<{ + data: TimelineEventsDetailsItem[]; +}> = ({ data }) => { + const ruleId = useMemo(() => { + const item = data.find((d) => d.field === 'signal.rule.id'); + return Array.isArray(item?.originalValue) + ? item?.originalValue[0] + : item?.originalValue ?? null; + }, [data]); + const { rule: maybeRule } = useRuleWithFallback(ruleId); + + if (!maybeRule?.note) { + return null; + } + + return ( + <> + + +
{i18n.INVESTIGATION_GUIDE}
+
+ + + + + {maybeRule.note} + + + + + ); +}; + +export const InvestigationGuideView = React.memo(InvestigationGuideViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx index ef7b9da69602..fc20f84d3650 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx @@ -36,7 +36,7 @@ export const FieldValueCell = React.memo( values, }: FieldValueCellProps) => { return ( -
+
{values != null && values.map((value, i) => { if (fieldFromBrowserField == null) { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx index 0fd7184e0c55..aecd702077d4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx @@ -11,7 +11,6 @@ import { cloneDeep } from 'lodash/fp'; import { render, screen } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n/react'; import { ThemeProvider } from 'styled-components'; -import { useRiskyHostLinks } from '../../containers/overview_risky_host_links/use_risky_host_links'; import { mockTheme } from '../overview_cti_links/mock'; import { RiskyHostLinks } from '.'; import { createStore, State } from '../../../common/store'; @@ -23,11 +22,12 @@ import { } from '../../../common/mock'; import { useRiskyHostsDashboardButtonHref } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; +import { useHostsRiskScore } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_risky_host_links/use_risky_host_links'); -const useRiskyHostLinksMock = useRiskyHostLinks as jest.Mock; +jest.mock('../../containers/overview_risky_host_links/use_hosts_risk_score'); +const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; jest.mock('../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'); const useRiskyHostsDashboardButtonHrefMock = useRiskyHostsDashboardButtonHref as jest.Mock; @@ -51,10 +51,10 @@ describe('RiskyHostLinks', () => { }); it('renders enabled module view if module is enabled', () => { - useRiskyHostLinksMock.mockReturnValueOnce({ + useHostsRiskScoreMock.mockReturnValueOnce({ loading: false, isModuleEnabled: true, - listItems: [], + result: [], }); render( @@ -62,10 +62,10 @@ describe('RiskyHostLinks', () => { @@ -76,10 +76,10 @@ describe('RiskyHostLinks', () => { }); it('renders disabled module view if module is disabled', () => { - useRiskyHostLinksMock.mockReturnValueOnce({ + useHostsRiskScoreMock.mockReturnValueOnce({ loading: false, isModuleEnabled: false, - listItems: [], + result: [], }); render( @@ -87,10 +87,10 @@ describe('RiskyHostLinks', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx index 895037170c44..57bcff45a634 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx @@ -7,18 +7,25 @@ import React from 'react'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { useRiskyHostLinks } from '../../containers/overview_risky_host_links/use_risky_host_links'; import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module'; import { RiskyHostsDisabledModule } from './risky_hosts_disabled_module'; -export type RiskyHostLinksProps = Pick; +import { useHostsRiskScore } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; +export interface RiskyHostLinksProps { + timerange: { to: string; from: string }; +} -const RiskyHostLinksComponent: React.FC = (props) => { - const { listItems, isModuleEnabled } = useRiskyHostLinks(props); +const RiskyHostLinksComponent: React.FC = ({ timerange }) => { + const hostRiskScore = useHostsRiskScore({ timerange }); - switch (isModuleEnabled) { + switch (hostRiskScore?.isModuleEnabled) { case true: - return ; + return ( + + ); case false: return ; case undefined: diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx index 7d8436bd9dd2..ae1a5f7b0284 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_disabled_module.tsx @@ -12,7 +12,7 @@ import { DisabledLinkPanel } from '../link_panel/disabled_link_panel'; import { RiskyHostsPanelView } from './risky_hosts_panel_view'; import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module'; -const RISKY_HOSTS_DOC_LINK = +export const RISKY_HOSTS_DOC_LINK = 'https://www.github.com/elastic/detection-rules/blob/main/docs/experimental-machine-learning/host-risk-score.md'; export const RiskyHostsDisabledModuleComponent = () => ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx index f751abdfb3ab..0126f115bec8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx @@ -52,7 +52,19 @@ describe('RiskyHostsEnabledModule', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx index f26e0c7fb433..4db6f67acb26 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx @@ -5,17 +5,32 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { RiskyHostsPanelView } from './risky_hosts_panel_view'; import { LinkPanelListItem } from '../link_panel'; import { useRiskyHostsDashboardButtonHref } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; +import { HostRisk } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; +import { HostsRiskScore } from '../../../../common'; + +const getListItemsFromHits = (items: HostsRiskScore[]): LinkPanelListItem[] => { + return items.map(({ host, risk_score: count, risk: copy }) => ({ + title: host.name, + count, + copy, + path: '', + })); +}; const RiskyHostsEnabledModuleComponent: React.FC<{ from: string; - listItems: LinkPanelListItem[]; + hostRiskScore: HostRisk; to: string; -}> = ({ listItems, to, from }) => { +}> = ({ hostRiskScore, to, from }) => { + const listItems = useMemo( + () => getListItemsFromHits(hostRiskScore?.result || []), + [hostRiskScore] + ); const { buttonHref } = useRiskyHostsDashboardButtonHref(to, from); const { listItemsWithLinks } = useRiskyHostsDashboardLinks(to, from, listItems); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx index e227e66a7d4f..84864902f75d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx @@ -14,7 +14,7 @@ import { LinkPanelViewProps } from '../link_panel/types'; import { Link } from '../link_panel/link'; import * as i18n from './translations'; import { VIEW_DASHBOARD } from '../overview_cti_links/translations'; -import { QUERY_ID as RiskyHostsQueryId } from '../../containers/overview_risky_host_links/use_risky_host_links'; +import { QUERY_ID as RiskyHostsQueryId } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; import { NavigateToHost } from './navigate_to_host'; const columns: Array> = [ diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_host_links.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts similarity index 53% rename from x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_host_links.ts rename to x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts index 7df091cbbd46..75cf51194ab6 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_host_links.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts @@ -9,61 +9,61 @@ import { i18n } from '@kbn/i18n'; import { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useRiskyHostsComplete } from './use_risky_hosts'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { useKibana } from '../../../common/lib/kibana'; import { inputsActions } from '../../../common/store/actions'; -import { LinkPanelListItem } from '../../components/link_panel'; -import { RISKY_HOSTS_INDEX } from '../../../../common/constants'; + +import { HOST_RISK_SCORES_INDEX } from '../../../../common/constants'; import { isIndexNotFoundError } from '../../../common/utils/exceptions'; +import { HostsRiskScore } from '../../../../common'; +import { useHostsRiskScoreComplete } from './use_hosts_risk_score_complete'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; -export const QUERY_ID = 'risky_hosts'; +export const QUERY_ID = 'host_risk_score'; const noop = () => {}; -export interface RiskyHost { - host: { - name: string; - }; - risk_score: number; - risk: string; -} - const isRecord = (item: unknown): item is Record => typeof item === 'object' && !!item; -const isRiskyHostHit = (item: unknown): item is RiskyHost => +const isHostsRiskScoreHit = (item: unknown): item is HostsRiskScore => isRecord(item) && isRecord(item.host) && typeof item.host.name === 'string' && typeof item.risk_score === 'number' && typeof item.risk === 'string'; -const getListItemsFromHits = (items: RiskyHost[]): LinkPanelListItem[] => { - return items.map(({ host, risk_score: count, risk: copy }) => ({ - title: host.name, - count, - copy, - path: '', - })); -}; +export interface HostRisk { + loading: boolean; + isModuleEnabled?: boolean; + result?: HostsRiskScore[]; +} -export const useRiskyHostLinks = ({ to, from }: { to: string; from: string }) => { +export const useHostsRiskScore = ({ + timerange, + hostName, +}: { + timerange?: { to: string; from: string }; + hostName?: string; +}): HostRisk | null => { + const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); const [isModuleEnabled, setIsModuleEnabled] = useState(undefined); + const [loading, setLoading] = useState(riskyHostsFeatureEnabled); const { addError } = useAppToasts(); const { data } = useKibana().services; const dispatch = useDispatch(); - const { error, loading, result, start } = useRiskyHostsComplete(); + const { error, result, start, loading: isHostsRiskScoreLoading } = useHostsRiskScoreComplete(); const deleteQuery = useCallback(() => { dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: QUERY_ID })); }, [dispatch]); useEffect(() => { - if (!loading && result) { + if (!isHostsRiskScoreLoading && result) { setIsModuleEnabled(true); + setLoading(false); dispatch( inputsActions.setQuery({ inputId: 'global', @@ -72,43 +72,51 @@ export const useRiskyHostLinks = ({ to, from }: { to: string; from: string }) => dsl: result.inspect?.dsl ?? [], response: [JSON.stringify(result.rawResponse, null, 2)], }, - loading, + loading: isHostsRiskScoreLoading, refetch: noop, }) ); } return deleteQuery; - }, [deleteQuery, dispatch, loading, result, setIsModuleEnabled]); + }, [deleteQuery, dispatch, isHostsRiskScoreLoading, result, setIsModuleEnabled]); useEffect(() => { if (error) { if (isIndexNotFoundError(error)) { setIsModuleEnabled(false); + setLoading(false); } else { addError(error, { - title: i18n.translate('xpack.securitySolution.overview.riskyHostsError', { - defaultMessage: 'Error Fetching Risky Hosts', + title: i18n.translate('xpack.securitySolution.overview.hostsRiskError', { + defaultMessage: 'Error Fetching Hosts Risk', }), }); + setLoading(false); setIsModuleEnabled(true); } } }, [addError, error, setIsModuleEnabled]); useEffect(() => { - start({ - data, - timerange: { to, from, interval: '' }, - defaultIndex: [RISKY_HOSTS_INDEX], - filterQuery: '', - }); - }, [start, data, to, from]); + if (riskyHostsFeatureEnabled && (hostName || timerange)) { + start({ + data, + timerange: timerange ? { to: timerange.to, from: timerange.from, interval: '' } : undefined, + hostName, + defaultIndex: [HOST_RISK_SCORES_INDEX], + }); + } + }, [start, data, timerange, hostName, riskyHostsFeatureEnabled]); + + if ((!hostName && !timerange) || !riskyHostsFeatureEnabled) { + return null; + } + + const hits = result?.rawResponse?.hits?.hits; return { - listItems: isRiskyHostHit(result?.rawResponse?.hits?.hits?.[0]?._source) - ? getListItemsFromHits( - result?.rawResponse?.hits?.hits?.map((hit) => hit._source) as RiskyHost[] - ) + result: isHostsRiskScoreHit(hits?.[0]?._source) + ? (hits?.map((hit) => hit._source) as HostsRiskScore[]) : [], isModuleEnabled, loading, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score_complete.ts similarity index 55% rename from x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts.ts rename to x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score_complete.ts index baf7606e8e23..22e3b5869212 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score_complete.ts @@ -15,28 +15,28 @@ import { } from '../../../../../../../src/plugins/data/public'; import { HostsQueries, - HostsRiskyHostsRequestOptions, - HostsRiskyHostsStrategyResponse, + HostsRiskScoreRequestOptions, + HostsRiskScoreStrategyResponse, } from '../../../../common'; -type GetRiskyHostsProps = HostsRiskyHostsRequestOptions & { +type GetHostsRiskScoreProps = HostsRiskScoreRequestOptions & { data: DataPublicPluginStart; signal: AbortSignal; }; -export const getRiskyHosts = ({ +export const getHostsRiskScore = ({ data, defaultIndex, - filterQuery, timerange, + hostName, signal, -}: GetRiskyHostsProps): Observable => - data.search.search( +}: GetHostsRiskScoreProps): Observable => + data.search.search( { defaultIndex, - factoryQueryType: HostsQueries.riskyHosts, - filterQuery, + factoryQueryType: HostsQueries.hostsRiskScore, timerange, + hostName, }, { strategy: 'securitySolutionSearchStrategy', @@ -44,16 +44,16 @@ export const getRiskyHosts = ({ } ); -export const getRiskyHostsComplete = ( - props: GetRiskyHostsProps -): Observable => { - return getRiskyHosts(props).pipe( +export const getHostsRiskScoreComplete = ( + props: GetHostsRiskScoreProps +): Observable => { + return getHostsRiskScore(props).pipe( filter((response) => { return isErrorResponse(response) || isCompleteResponse(response); }) ); }; -const getRiskyHostsWithOptionalSignal = withOptionalSignal(getRiskyHostsComplete); +const getHostsRiskScoreWithOptionalSignal = withOptionalSignal(getHostsRiskScoreComplete); -export const useRiskyHostsComplete = () => useObservable(getRiskyHostsWithOptionalSignal); +export const useHostsRiskScoreComplete = () => useObservable(getHostsRiskScoreWithOptionalSignal); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 66f431522176..cab02450f888 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -31,8 +31,8 @@ import { } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges'; -import { useRiskyHostLinks } from '../containers/overview_risky_host_links/use_risky_host_links'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -86,9 +86,9 @@ jest.mock('../containers/overview_cti_links/use_is_threat_intel_module_enabled') const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock; useIsThreatIntelModuleEnabledMock.mockReturnValue(true); -jest.mock('../containers/overview_risky_host_links/use_risky_host_links'); -const useRiskyHostLinksMock = useRiskyHostLinks as jest.Mock; -useRiskyHostLinksMock.mockReturnValue({ +jest.mock('../containers/overview_risky_host_links/use_hosts_risk_score'); +const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; +useHostsRiskScoreMock.mockReturnValue({ loading: false, isModuleEnabled: false, listItems: [], diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 93c0bac5a88d..10fa4e4c4e92 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -164,10 +164,10 @@ const OverviewComponent = () => { {riskyHostsEnabled && ( )} 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 b2a8a439220c..137d8d78bcda 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 @@ -435,6 +435,7 @@ exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should "indexName": "my-index", } } + hostRisk={null} isAlert={false} isDraggable={false} loading={true} @@ -954,6 +955,7 @@ Array [ "indexName": "my-index", } } + hostRisk={null} isAlert={false} isDraggable={false} loading={true} @@ -1990,6 +1992,7 @@ Array [ "indexName": "my-index", } } + hostRisk={null} isAlert={false} isDraggable={false} loading={true} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index b3d7b869c069..53382fe8fa21 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -22,6 +22,7 @@ import { BrowserFields } from '../../../../common/containers/source'; import { EventDetails } from '../../../../common/components/event_details/event_details'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import * as i18n from './translations'; +import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; export type HandleOnEventClosed = () => void; interface Props { @@ -34,6 +35,7 @@ interface Props { messageHeight?: number; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; + hostRisk: HostRisk | null; } interface ExpandableEventTitleProps { @@ -90,6 +92,7 @@ export const ExpandableEvent = React.memo( isDraggable, loading, detailsData, + hostRisk, }) => { if (!event.eventId) { return {i18n.EVENT_DETAILS_PLACEHOLDER}; @@ -110,6 +113,7 @@ export const ExpandableEvent = React.memo( isDraggable={isDraggable} timelineId={timelineId} timelineTabType={timelineTabType} + hostRisk={hostRisk} /> 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 ba58e8a08406..f8786e070683 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 @@ -34,6 +34,7 @@ import { TimelineNonEcsData } from '../../../../../common'; import { Ecs } from '../../../../../common/ecs'; import { EventDetailsFooter } from './footer'; import { EntityType } from '../../../../../../timelines/common'; +import { useHostsRiskScore } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { @@ -124,6 +125,10 @@ const EventDetailsPanelComponent: React.FC = ({ [detailsData] ); + const hostRisk = useHostsRiskScore({ + hostName, + }); + const backToAlertDetailsLink = useMemo(() => { return ( <> @@ -192,6 +197,7 @@ const EventDetailsPanelComponent: React.FC = ({ loading={loading} timelineId={timelineId} timelineTabType="flyout" + hostRisk={hostRisk} /> )} @@ -224,6 +230,7 @@ const EventDetailsPanelComponent: React.FC = ({ loading={loading} timelineId={timelineId} timelineTabType={tabType} + hostRisk={hostRisk} /> ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts index b807806e1809..9aef01d953c8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts @@ -10,7 +10,8 @@ import { HostsQueries, HostsKpiQueries } from '../../../../../common/search_stra import { allHosts } from './all'; import { hostDetails } from './details'; import { hostOverview } from './overview'; -import { riskyHosts } from './risky_hosts'; + +import { riskScore } from './risk_score'; import { firstOrLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications, authenticationsEntities } from './authentications'; @@ -27,7 +28,7 @@ jest.mock('./authentications'); jest.mock('./kpi/authentications'); jest.mock('./kpi/hosts'); jest.mock('./kpi/unique_ips'); -jest.mock('./risky_hosts'); +jest.mock('./risk_score'); describe('hostsFactory', () => { test('should include correct apis', () => { @@ -39,7 +40,7 @@ describe('hostsFactory', () => { [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, [HostsQueries.authenticationsEntities]: authenticationsEntities, - [HostsQueries.riskyHosts]: riskyHosts, + [HostsQueries.hostsRiskScore]: riskScore, [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, [HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities, [HostsKpiQueries.kpiHosts]: hostsKpiHosts, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index d067dacfc529..5b501099a21e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -21,7 +21,7 @@ import { authentications, authenticationsEntities } from './authentications'; import { hostsKpiAuthentications, hostsKpiAuthenticationsEntities } from './kpi/authentications'; import { hostsKpiHosts, hostsKpiHostsEntities } from './kpi/hosts'; import { hostsKpiUniqueIps, hostsKpiUniqueIpsEntities } from './kpi/unique_ips'; -import { riskyHosts } from './risky_hosts'; +import { riskScore } from './risk_score'; export const hostsFactory: Record< HostsQueries | HostsKpiQueries, @@ -35,7 +35,7 @@ export const hostsFactory: Record< [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, [HostsQueries.authenticationsEntities]: authenticationsEntities, - [HostsQueries.riskyHosts]: riskyHosts, + [HostsQueries.hostsRiskScore]: riskScore, [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, [HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities, [HostsKpiQueries.kpiHosts]: hostsKpiHosts, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risky_hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/index.ts similarity index 51% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risky_hosts/index.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/index.ts index 0b2fd1c00c3d..2a440ad614d9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risky_hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/index.ts @@ -6,23 +6,23 @@ */ import { SecuritySolutionFactory } from '../../types'; -import { HostsQueries } from '../../../../../../common'; +import { + HostsRiskScoreRequestOptions, + HostsQueries, + HostsRiskScoreStrategyResponse, +} from '../../../../../../common'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { inspectStringifyObject } from '../../../../../utils/build_query'; -import { buildRiskyHostsQuery } from './query.risky_hosts.dsl'; -import { - HostsRiskyHostsRequestOptions, - HostsRiskyHostsStrategyResponse, -} from '../../../../../../common/search_strategy/security_solution/hosts/risky_hosts'; +import { buildHostsRiskScoreQuery } from './query.hosts_risk.dsl'; -export const riskyHosts: SecuritySolutionFactory = { - buildDsl: (options: HostsRiskyHostsRequestOptions) => buildRiskyHostsQuery(options), +export const riskScore: SecuritySolutionFactory = { + buildDsl: (options: HostsRiskScoreRequestOptions) => buildHostsRiskScoreQuery(options), parse: async ( - options: HostsRiskyHostsRequestOptions, + options: HostsRiskScoreRequestOptions, response: IEsSearchResponse - ): Promise => { + ): Promise => { const inspect = { - dsl: [inspectStringifyObject(buildRiskyHostsQuery(options))], + dsl: [inspectStringifyObject(buildHostsRiskScoreQuery(options))], }; return { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risky_hosts/query.risky_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts similarity index 58% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risky_hosts/query.risky_hosts.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts index 79b6a91ff403..43930ab3de2e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risky_hosts/query.risky_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts @@ -5,26 +5,30 @@ * 2.0. */ -import { HostsRiskyHostsRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts/risky_hosts'; -import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { HostsRiskScoreRequestOptions } from '../../../../../../common'; -export const buildRiskyHostsQuery = ({ - filterQuery, - timerange: { from, to }, +export const buildHostsRiskScoreQuery = ({ + timerange, + hostName, defaultIndex, -}: HostsRiskyHostsRequestOptions) => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - { +}: HostsRiskScoreRequestOptions) => { + const filter = []; + + if (timerange) { + filter.push({ range: { '@timestamp': { - gte: from, - lte: to, + gte: timerange.from, + lte: timerange.to, format: 'strict_date_optional_time', }, }, - }, - ]; + }); + } + + if (hostName) { + filter.push({ term: { 'host.name': hostName } }); + } const dslQuery = { index: defaultIndex, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 245245a0a459..2461d3b58b07 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22351,9 +22351,7 @@ "xpack.securitySolution.eventDetails.copyToClipboard": "クリップボードにコピー", "xpack.securitySolution.eventDetails.copyToClipboardTooltip": "クリップボードにコピー", "xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent": "このフィールド値は、作成したルールの脅威インテリジェンス指標と一致しました。", - "xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle": "脅威一致が検出されました", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "このフィールド値には脅威インテリジェンスソースの別の情報があります。", - "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle": "Threat Intelligenceで拡張", "xpack.securitySolution.eventDetails.ctiSummary.providerPreposition": "開始", "xpack.securitySolution.eventDetails.description": "説明", "xpack.securitySolution.eventDetails.field": "フィールド", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3c1750aabd34..860a91676b3e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22700,9 +22700,7 @@ "xpack.securitySolution.eventDetails.copyToClipboard": "复制到剪贴板", "xpack.securitySolution.eventDetails.copyToClipboardTooltip": "复制到剪贴板", "xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipContent": "此字段值使用您创建的规则匹配威胁情报指标。", - "xpack.securitySolution.eventDetails.ctiSummary.indicatorEnrichmentTooltipTitle": "检测到威胁匹配", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "此字段值具有威胁情报源提供的其他信息。", - "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipTitle": "已使用威胁情报扩充", "xpack.securitySolution.eventDetails.ctiSummary.providerPreposition": "来自", "xpack.securitySolution.eventDetails.description": "描述", "xpack.securitySolution.eventDetails.field": "字段",