diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx new file mode 100644 index 000000000000..f93721349fda --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { HostStatus } from '../../../../common/endpoint/types'; +import { HOST_STATUS_TO_BADGE_COLOR } from '../../../management/pages/endpoint_hosts/view/host_constants'; + +export const AgentStatus = React.memo(({ hostStatus }: { hostStatus: HostStatus }) => { + return ( + + + + ); +}); + +AgentStatus.displayName = 'AgentStatus'; 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 5578264152c3..e229c0c6fae4 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 @@ -12,7 +12,7 @@ import { EuiDescriptionListTitle, EuiSpacer, } from '@elastic/eui'; -import { get, getOr } from 'lodash/fp'; +import { get, getOr, find } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; @@ -53,6 +53,7 @@ const fields = [ { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, { id: 'host.name' }, + { id: 'host.status' }, { id: 'user.name' }, { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, @@ -177,6 +178,24 @@ const AlertSummaryViewComponent: React.FC<{ timelineId, ]); + const agentId = useMemo(() => { + const findAgentId = find({ category: 'agent', field: 'agent.id' }, data)?.values; + return findAgentId ? findAgentId[0] : ''; + }, [data]); + + const agentStatusRow = { + title: i18n.AGENT_STATUS, + description: { + contextId: timelineId, + eventId, + fieldName: 'host.status', + value: agentId, + linkValue: undefined, + }, + }; + + const summaryRowsWithAgentStatus = [...summaryRows, agentStatusRow]; + const ruleId = useMemo(() => { const item = data.find((d) => d.field === 'signal.rule.id'); return Array.isArray(item?.originalValue) @@ -188,7 +207,11 @@ const AlertSummaryViewComponent: React.FC<{ return ( <> - + {maybeRule?.note && ( {i18n.INVESTIGATION_GUIDE} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 1ff88d9c2018..a28d1976ca94 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -99,3 +99,7 @@ export const NESTED_COLUMN = (field: string) => defaultMessage: 'The {field} field is an object, and is broken down into nested fields which can be added as column', }); + +export const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', { + defaultMessage: 'Agent status', +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 71eac547dcc8..05706981a681 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -7,7 +7,7 @@ import { UpdateDocumentByQueryResponse } from 'elasticsearch'; import { getCasesFromAlertsUrl } from '../../../../../../cases/common'; -import { HostIsolationResponse, HostMetadataInfo } from '../../../../../common/endpoint/types'; +import { HostIsolationResponse, HostInfo } from '../../../../../common/endpoint/types'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -178,12 +178,8 @@ export const getCaseIdsFromAlertId = async ({ * * @param host id */ -export const getHostMetadata = async ({ - agentId, -}: { - agentId: string; -}): Promise => - KibanaServices.get().http.fetch( +export const getHostMetadata = async ({ agentId }: { agentId: string }): Promise => + KibanaServices.get().http.fetch( resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }), { method: 'get' } ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx index f7894d476427..7419727fff6a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx @@ -7,25 +7,27 @@ import { isEmpty } from 'lodash'; import { useEffect, useState } from 'react'; -import { Maybe } from '../../../../../../observability/common/typings'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { getHostMetadata } from './api'; import { ISOLATION_STATUS_FAILURE } from './translations'; import { isEndpointHostIsolated } from '../../../../common/utils/validators'; +import { HostStatus } from '../../../../../common/endpoint/types'; interface HostIsolationStatusResponse { loading: boolean; - isIsolated: Maybe; + isIsolated: boolean; + agentStatus: HostStatus; } /* - * Retrieves the current isolation status of a host */ + * Retrieves the current isolation status of a host and the agent/host status */ export const useHostIsolationStatus = ({ agentId, }: { agentId: string; }): HostIsolationStatusResponse => { - const [isIsolated, setIsIsolated] = useState>(); + const [isIsolated, setIsIsolated] = useState(false); + const [agentStatus, setAgentStatus] = useState(HostStatus.UNHEALTHY); const [loading, setLoading] = useState(false); const { addError } = useAppToasts(); @@ -38,6 +40,7 @@ export const useHostIsolationStatus = ({ const metadataResponse = await getHostMetadata({ agentId }); if (isMounted) { setIsIsolated(isEndpointHostIsolated(metadataResponse.metadata)); + setAgentStatus(metadataResponse.host_status); } } catch (error) { addError(error.message, { title: ISOLATION_STATUS_FAILURE }); @@ -61,5 +64,5 @@ export const useHostIsolationStatus = ({ isMounted = false; }; }, [addError, agentId]); - return { loading, isIsolated }; + return { loading, isIsolated, agentStatus }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx new file mode 100644 index 000000000000..16f11809dd72 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { DefaultDraggable } from '../../../../../common/components/draggables'; +import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation'; +import { useHostIsolationStatus } from '../../../../../detections/containers/detection_engine/alerts/use_host_isolation_status'; +import { AgentStatus } from '../../../../../common/components/endpoint/agent_status'; + +export const AgentStatuses = React.memo( + ({ + fieldName, + contextId, + eventId, + value, + }: { + fieldName: string; + contextId: string; + eventId: string; + value: string; + }) => { + const { isIsolated, agentStatus } = useHostIsolationStatus({ agentId: value }); + const isolationFieldName = 'host.isolation'; + return ( + + + + + + + + + + + + + ); + } +); + +AgentStatuses.displayName = 'AgentStatuses'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index b1ef634fe052..761d82b482af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -16,3 +16,4 @@ export const REFERENCE_URL_FIELD_NAME = 'reference.url'; export const EVENT_URL_FIELD_NAME = 'event.url'; export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name'; export const SIGNAL_STATUS_FIELD_NAME = 'signal.status'; +export const HOST_STATUS_FIELD_NAME = 'host.status'; 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 12effcd3fa81..efb51916e376 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 @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable complexity */ + import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { isNumber, isEmpty } from 'lodash/fp'; import React from 'react'; @@ -30,11 +32,13 @@ import { REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME, SIGNAL_STATUS_FIELD_NAME, + HOST_STATUS_FIELD_NAME, GEO_FIELD_TYPE, } from './constants'; import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers'; import { RuleStatus } from './rule_status'; import { HostName } from './host_name'; +import { AgentStatuses } from './agent_statuses'; // simple black-list to prevent dragging and dropping fields such as message name const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; @@ -116,6 +120,15 @@ const FormattedFieldValueComponent: React.FC<{ return ( ); + } else if (fieldName === HOST_STATUS_FIELD_NAME) { + return ( + + ); } else if ( [ RULE_REFERENCE_FIELD_NAME,