mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Host Isolation] Host Isolation Flyout UI only (#96077)
Co-authored-by: pzl <dan@panzarel.la>
This commit is contained in:
parent
fb34ba0337
commit
a1cb79b3d2
14 changed files with 552 additions and 14 deletions
|
@ -25,3 +25,6 @@ export const TRUSTED_APPS_SUMMARY_API = '/api/endpoint/trusted_apps/summary';
|
|||
export const BASE_POLICY_RESPONSE_ROUTE = `/api/endpoint/policy_response`;
|
||||
export const BASE_POLICY_ROUTE = `/api/endpoint/policy`;
|
||||
export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`;
|
||||
|
||||
/** Host Isolation Routes */
|
||||
export const HOST_ISOLATION_CREATE_API = `/api/endpoint/isolate`;
|
||||
|
|
|
@ -14,6 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
|
|||
const allowedExperimentalValues = Object.freeze({
|
||||
trustedAppsByPolicyEnabled: false,
|
||||
eventFilteringEnabled: false,
|
||||
hostIsolationEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -42,6 +42,7 @@ export const mockGlobalState: State = {
|
|||
enableExperimental: {
|
||||
eventFilteringEnabled: false,
|
||||
trustedAppsByPolicyEnabled: false,
|
||||
hostIsolationEnabled: false,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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, { useMemo, useState, useCallback } from 'react';
|
||||
import { find } from 'lodash/fp';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiTextArea,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
import {
|
||||
CANCEL,
|
||||
CASES_ASSOCIATED_WITH_ALERT,
|
||||
COMMENT,
|
||||
COMMENT_PLACEHOLDER,
|
||||
CONFIRM,
|
||||
RETURN_TO_ALERT_DETAILS,
|
||||
} from './translations';
|
||||
import { Maybe } from '../../../../../observability/common/typings';
|
||||
|
||||
export const HostIsolationPanel = React.memo(
|
||||
({
|
||||
details,
|
||||
cancelCallback,
|
||||
}: {
|
||||
details: Maybe<TimelineEventsDetailsItem[]>;
|
||||
cancelCallback: () => void;
|
||||
}) => {
|
||||
const [comment, setComment] = useState('');
|
||||
const [isIsolated, setIsIsolated] = useState(false);
|
||||
|
||||
const agentId = useMemo(() => {
|
||||
const findAgentId = find({ category: 'agent', field: 'agent.id' }, details)?.values;
|
||||
return findAgentId ? findAgentId[0] : '';
|
||||
}, [details]);
|
||||
|
||||
const hostName = useMemo(() => {
|
||||
const findHostName = find({ category: 'host', field: 'host.name' }, details)?.values;
|
||||
return findHostName ? findHostName[0] : '';
|
||||
}, [details]);
|
||||
|
||||
const alertRule = useMemo(() => {
|
||||
const findAlertRule = find({ category: 'signal', field: 'signal.rule.name' }, details)
|
||||
?.values;
|
||||
return findAlertRule ? findAlertRule[0] : '';
|
||||
}, [details]);
|
||||
|
||||
const { loading, isolateHost } = useHostIsolation({ agentId, comment });
|
||||
|
||||
const confirmHostIsolation = useCallback(async () => {
|
||||
const hostIsolated = await isolateHost();
|
||||
setIsIsolated(hostIsolated);
|
||||
}, [isolateHost]);
|
||||
|
||||
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
||||
|
||||
// a placeholder until we get the case count returned from a new case route in a future pr
|
||||
const caseCount: number = 0;
|
||||
|
||||
const hostIsolated = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
iconType="check"
|
||||
color="success"
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.title',
|
||||
{
|
||||
defaultMessage: 'Host Isolation on {hostname} successfully submitted',
|
||||
values: { hostname: hostName },
|
||||
}
|
||||
)}
|
||||
>
|
||||
{caseCount > 0 && (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases"
|
||||
defaultMessage="This case has been attached to the following {caseCount, plural, one {case} other {cases}}:"
|
||||
values={{ caseCount }}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiText size="s">
|
||||
<ul>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.hostIsolation.placeholderCase"
|
||||
defaultMessage="Case"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</EuiCallOut>
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty flush="right" onClick={backToAlertDetails}>
|
||||
<EuiText size="s">
|
||||
<p>{RETURN_TO_ALERT_DETAILS}</p>
|
||||
</EuiText>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}, [backToAlertDetails, hostName]);
|
||||
|
||||
const hostNotIsolated = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost"
|
||||
defaultMessage="Isolate host {hostname} from network. This action will be added to the {cases}."
|
||||
values={{
|
||||
hostname: <b>{hostName}</b>,
|
||||
cases: (
|
||||
<b>
|
||||
{caseCount}
|
||||
{CASES_ASSOCIATED_WITH_ALERT}
|
||||
{alertRule}
|
||||
</b>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xs">
|
||||
<h4>{COMMENT}</h4>
|
||||
</EuiTitle>
|
||||
<EuiTextArea
|
||||
data-test-subj="host_isolation_comment"
|
||||
fullWidth={true}
|
||||
placeholder={COMMENT_PLACEHOLDER}
|
||||
value={comment}
|
||||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setComment(event.target.value)
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={backToAlertDetails}>{CANCEL}</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={confirmHostIsolation} isLoading={loading}>
|
||||
{CONFIRM}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}, [alertRule, backToAlertDetails, comment, confirmHostIsolation, hostName, loading]);
|
||||
|
||||
return isIsolated ? hostIsolated : hostNotIsolated;
|
||||
}
|
||||
);
|
||||
|
||||
HostIsolationPanel.displayName = 'HostIsolationContent';
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiContextMenuItem, EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
|
||||
import { ISOLATE_HOST } from './translations';
|
||||
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
|
||||
|
||||
export const TakeActionDropdown = React.memo(
|
||||
({ onChange }: { onChange: (action: 'isolateHost') => void }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const closePopoverHandler = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const takeActionItems = useMemo(() => {
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
key="isolateHost"
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
onChange('isolateHost');
|
||||
}}
|
||||
>
|
||||
{ISOLATE_HOST}
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
}, [onChange]);
|
||||
|
||||
const takeActionButton = useMemo(() => {
|
||||
return (
|
||||
<EuiButton
|
||||
iconSide="right"
|
||||
fill
|
||||
iconType="arrowDown"
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
>
|
||||
{TAKE_ACTION}
|
||||
</EuiButton>
|
||||
);
|
||||
}, [isPopoverOpen]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="hostIsolationTakeActionPanel"
|
||||
button={takeActionButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopoverHandler}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={takeActionItems} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TakeActionDropdown.displayName = 'TakeActionDropdown';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ISOLATE_HOST = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.hostIsolation.isolateHost.isolateHost',
|
||||
{
|
||||
defaultMessage: 'Isolate host',
|
||||
}
|
||||
);
|
||||
|
||||
export const COMMENT = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.comment', {
|
||||
defaultMessage: 'Comment',
|
||||
});
|
||||
|
||||
export const COMMENT_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.hostIsolation.comment.placeholder',
|
||||
{ defaultMessage: 'You may leave an optional note here.' }
|
||||
);
|
||||
|
||||
export const CANCEL = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
});
|
||||
|
||||
export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.confirm', {
|
||||
defaultMessage: 'Confirm',
|
||||
});
|
||||
|
||||
export const CASES_ASSOCIATED_WITH_ALERT = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWihtAlert',
|
||||
{
|
||||
defaultMessage: ' cases associated with the rule ',
|
||||
}
|
||||
);
|
||||
|
||||
export const RETURN_TO_ALERT_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.hostIsolation.returnToAlertDetails',
|
||||
{ defaultMessage: 'Return to alert details' }
|
||||
);
|
|
@ -12,6 +12,7 @@ import {
|
|||
DETECTION_ENGINE_INDEX_URL,
|
||||
DETECTION_ENGINE_PRIVILEGES_URL,
|
||||
} from '../../../../../common/constants';
|
||||
import { HOST_ISOLATION_CREATE_API } from '../../../../../common/endpoint/constants';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
BasicSignals,
|
||||
|
@ -20,6 +21,7 @@ import {
|
|||
AlertSearchResponse,
|
||||
AlertsIndex,
|
||||
UpdateAlertStatusProps,
|
||||
HostIsolationResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
|
@ -101,3 +103,26 @@ export const createSignalIndex = async ({ signal }: BasicSignals): Promise<Alert
|
|||
method: 'POST',
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Host Isolation index
|
||||
*
|
||||
* @param agent id
|
||||
* @param optional comment for the isolation action
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const createHostIsolation = async ({
|
||||
agentId,
|
||||
comment = '',
|
||||
}: {
|
||||
agentId: string;
|
||||
comment?: string;
|
||||
}): Promise<HostIsolationResponse> =>
|
||||
KibanaServices.get().http.fetch<HostIsolationResponse>(HOST_ISOLATION_CREATE_API, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
agent_ids: [agentId],
|
||||
comment,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -34,3 +34,8 @@ export const SIGNAL_POST_FAILURE = i18n.translate(
|
|||
defaultMessage: 'Failed to create signal index',
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_ISOLATION_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.hostIsolation.failedToIsolate.title',
|
||||
{ defaultMessage: 'Failed to isolate host' }
|
||||
);
|
||||
|
|
|
@ -48,6 +48,10 @@ export interface AlertsIndex {
|
|||
index_mapping_outdated: boolean;
|
||||
}
|
||||
|
||||
export interface HostIsolationResponse {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface Privilege {
|
||||
username: string;
|
||||
has_all_requested: boolean;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { useCallback, useState } from 'react';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { HOST_ISOLATION_FAILURE } from './translations';
|
||||
import { createHostIsolation } from './api';
|
||||
|
||||
interface HostIsolationStatus {
|
||||
loading: boolean;
|
||||
isolateHost: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
interface UseHostIsolationProps {
|
||||
agentId: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export const useHostIsolation = ({
|
||||
agentId,
|
||||
comment,
|
||||
}: UseHostIsolationProps): HostIsolationStatus => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
const isolateHost = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const isolationStatus = await createHostIsolation({ agentId, comment });
|
||||
setLoading(false);
|
||||
return isolationStatus.action ? true : false;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
addError(error.message, { title: HOST_ISOLATION_FAILURE });
|
||||
return false;
|
||||
}
|
||||
}, [agentId, comment, addError]);
|
||||
return { loading, isolateHost };
|
||||
};
|
|
@ -5,16 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { some } from 'lodash/fp';
|
||||
import { EuiFlyoutHeader, EuiFlyoutBody, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { find, some } from 'lodash/fp';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { BrowserFields, DocValueFields } from '../../../../common/containers/source';
|
||||
import { ExpandableEvent, ExpandableEventTitle } from './expandable_event';
|
||||
import { useTimelineEventsDetails } from '../../../containers/details';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { HostIsolationPanel } from '../../../../detections/components/host_isolation';
|
||||
import { TakeActionDropdown } from '../../../../detections/components/host_isolation/take_action_dropdown';
|
||||
import { ISOLATE_HOST } from '../../../../detections/components/host_isolation/translations';
|
||||
import { ALERT_DETAILS } from './translations';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflow {
|
||||
|
@ -56,8 +70,47 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
skip: !expandedEvent.eventId,
|
||||
});
|
||||
|
||||
const isHostIsolationEnabled = useIsExperimentalFeatureEnabled('hostIsolationEnabled');
|
||||
|
||||
const [isHostIsolationPanelOpen, setIsHostIsolationPanel] = useState(false);
|
||||
|
||||
const showAlertDetails = useCallback(() => {
|
||||
setIsHostIsolationPanel(false);
|
||||
}, []);
|
||||
|
||||
const showHostIsolationPanel = useCallback((action) => {
|
||||
if (action === 'isolateHost') {
|
||||
setIsHostIsolationPanel(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, detailsData);
|
||||
|
||||
const isEndpointAlert = useMemo(() => {
|
||||
const findEndpointAlert = find({ category: 'agent', field: 'agent.type' }, detailsData)?.values;
|
||||
return findEndpointAlert ? findEndpointAlert[0] === 'endpoint' : false;
|
||||
}, [detailsData]);
|
||||
|
||||
const backToAlertDetailsLink = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
iconSide="left"
|
||||
flush="left"
|
||||
onClick={() => showAlertDetails()}
|
||||
>
|
||||
<EuiText size="xs">
|
||||
<p>{ALERT_DETAILS}</p>
|
||||
</EuiText>
|
||||
</EuiButtonEmpty>
|
||||
<EuiTitle>
|
||||
<h2>{ISOLATE_HOST}</h2>
|
||||
</EuiTitle>
|
||||
</>
|
||||
);
|
||||
}, [showAlertDetails]);
|
||||
|
||||
if (!expandedEvent?.eventId) {
|
||||
return null;
|
||||
}
|
||||
|
@ -65,19 +118,38 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
return isFlyoutView ? (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<ExpandableEventTitle isAlert={isAlert} loading={loading} />
|
||||
{isHostIsolationPanelOpen ? (
|
||||
backToAlertDetailsLink
|
||||
) : (
|
||||
<ExpandableEventTitle isAlert={isAlert} loading={loading} />
|
||||
)}
|
||||
</EuiFlyoutHeader>
|
||||
<StyledEuiFlyoutBody>
|
||||
<ExpandableEvent
|
||||
browserFields={browserFields}
|
||||
detailsData={detailsData}
|
||||
event={expandedEvent}
|
||||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
timelineTabType="flyout"
|
||||
/>
|
||||
{isHostIsolationPanelOpen ? (
|
||||
<HostIsolationPanel details={detailsData} cancelCallback={showAlertDetails} />
|
||||
) : (
|
||||
<ExpandableEvent
|
||||
browserFields={browserFields}
|
||||
detailsData={detailsData}
|
||||
event={expandedEvent}
|
||||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
timelineTabType="flyout"
|
||||
/>
|
||||
)}
|
||||
</StyledEuiFlyoutBody>
|
||||
{isHostIsolationEnabled && isEndpointAlert && isHostIsolationPanelOpen === false && (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeActionDropdown onChange={showHostIsolationPanel} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlyoutFooter>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './isolation';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
import { IRouter } from 'src/core/server';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
/**
|
||||
* Registers the Host-(un-)isolation routes
|
||||
*/
|
||||
export function registerHostIsolationRoutes(router: IRouter, endpointContext: EndpointAppContext) {
|
||||
// perform isolation
|
||||
router.post(
|
||||
{
|
||||
path: `/api/endpoint/isolate`,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
agent_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
endpoint_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
alert_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
case_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
comment: schema.nullable(schema.string()),
|
||||
}),
|
||||
},
|
||||
options: { authRequired: true },
|
||||
},
|
||||
async (context, req, res) => {
|
||||
if (
|
||||
(req.body.agent_ids === null || req.body.agent_ids.length === 0) &&
|
||||
(req.body.endpoint_ids === null || req.body.endpoint_ids.length === 0)
|
||||
) {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
message: 'At least one agent ID or endpoint ID is required',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
action: '713085d6-ab45-4e9e-b41d-96563cafdd97',
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// perform UN-isolate
|
||||
router.post(
|
||||
{
|
||||
path: `/api/endpoint/unisolate`,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
agent_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
endpoint_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
alert_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
case_ids: schema.nullable(schema.arrayOf(schema.string())),
|
||||
comment: schema.nullable(schema.string()),
|
||||
}),
|
||||
},
|
||||
options: { authRequired: true },
|
||||
},
|
||||
async (context, req, res) => {
|
||||
if (
|
||||
(req.body.agent_ids === null || req.body.agent_ids.length === 0) &&
|
||||
(req.body.endpoint_ids === null || req.body.endpoint_ids.length === 0)
|
||||
) {
|
||||
return res.badRequest({
|
||||
body: {
|
||||
message: 'At least one agent ID or endpoint ID is required',
|
||||
},
|
||||
});
|
||||
}
|
||||
return res.ok({
|
||||
body: {
|
||||
action: '53ba1dd1-58a7-407e-b2a9-6843d9980068',
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -58,6 +58,7 @@ import { registerEndpointRoutes } from './endpoint/routes/metadata';
|
|||
import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency';
|
||||
import { registerResolverRoutes } from './endpoint/routes/resolver';
|
||||
import { registerPolicyRoutes } from './endpoint/routes/policy';
|
||||
import { registerHostIsolationRoutes } from './endpoint/routes/actions';
|
||||
import { EndpointArtifactClient, ManifestManager } from './endpoint/services';
|
||||
import { EndpointAppContextService } from './endpoint/endpoint_app_context_services';
|
||||
import { EndpointAppContext } from './endpoint/types';
|
||||
|
@ -205,6 +206,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
registerResolverRoutes(router);
|
||||
registerPolicyRoutes(router, endpointContext);
|
||||
registerTrustedAppsRoutes(router, endpointContext);
|
||||
registerHostIsolationRoutes(router, endpointContext);
|
||||
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: SERVER_APP_ID,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue