[Security Solution][Endpoint][Host Isolation] Isolation status badge from alert details (#102274) (#102686)

This commit is contained in:
Candace Park 2021-06-21 00:19:31 -04:00 committed by GitHub
parent b3a59f504c
commit bd9feb921d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 14 deletions

View file

@ -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';

View file

@ -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>

View file

@ -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',
});

View file

@ -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' }
);

View file

@ -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 };
};

View file

@ -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';

View file

@ -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';

View file

@ -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,