mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Endpoint][Host Isolation] Isolation status badge from alert details (#102274) (#102686)
This commit is contained in:
parent
b3a59f504c
commit
bd9feb921d
8 changed files with 140 additions and 14 deletions
|
@ -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 (
|
||||
<EuiBadge
|
||||
color={hostStatus != null ? HOST_STATUS_TO_BADGE_COLOR[hostStatus] : 'warning'}
|
||||
data-test-subj="rowHostStatus"
|
||||
className="eui-textTruncate"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.hostStatusValue"
|
||||
defaultMessage="{hostStatus, select, healthy {Healthy} unhealthy {Unhealthy} updating {Updating} offline {Offline} inactive {Inactive} other {Unhealthy}}"
|
||||
values={{ hostStatus }}
|
||||
/>
|
||||
</EuiBadge>
|
||||
);
|
||||
});
|
||||
|
||||
AgentStatus.displayName = 'AgentStatus';
|
|
@ -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 (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<SummaryView summaryColumns={summaryColumns} summaryRows={summaryRows} title={title} />
|
||||
<SummaryView
|
||||
summaryColumns={summaryColumns}
|
||||
summaryRows={summaryRowsWithAgentStatus}
|
||||
title={title}
|
||||
/>
|
||||
{maybeRule?.note && (
|
||||
<StyledEuiDescriptionList data-test-subj={`summary-view-guide`} compressed>
|
||||
<EuiDescriptionListTitle>{i18n.INVESTIGATION_GUIDE}</EuiDescriptionListTitle>
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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<HostMetadataInfo> =>
|
||||
KibanaServices.get().http.fetch<HostMetadataInfo>(
|
||||
export const getHostMetadata = async ({ agentId }: { agentId: string }): Promise<HostInfo> =>
|
||||
KibanaServices.get().http.fetch<HostInfo>(
|
||||
resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }),
|
||||
{ method: 'get' }
|
||||
);
|
||||
|
|
|
@ -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<boolean>;
|
||||
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<Maybe<boolean>>();
|
||||
const [isIsolated, setIsIsolated] = useState<boolean>(false);
|
||||
const [agentStatus, setAgentStatus] = useState<HostStatus>(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 };
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
|
||||
tooltipContent={fieldName}
|
||||
value={`${agentStatus}`}
|
||||
>
|
||||
<AgentStatus hostStatus={agentStatus} />
|
||||
</DefaultDraggable>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<DefaultDraggable
|
||||
field={isolationFieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${isolationFieldName}-${value}`}
|
||||
tooltipContent={isolationFieldName}
|
||||
value={`${isIsolated}`}
|
||||
>
|
||||
<EndpointHostIsolationStatus isIsolated={isIsolated} />
|
||||
</DefaultDraggable>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AgentStatuses.displayName = 'AgentStatuses';
|
|
@ -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';
|
||||
|
|
|
@ -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 (
|
||||
<RuleStatus contextId={contextId} eventId={eventId} fieldName={fieldName} value={value} />
|
||||
);
|
||||
} else if (fieldName === HOST_STATUS_FIELD_NAME) {
|
||||
return (
|
||||
<AgentStatuses
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
value={value as string}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
[
|
||||
RULE_REFERENCE_FIELD_NAME,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue