[Security Solution][Endpoint] Refactor Host Isolation component used in Isolate use case (#100159)
* EndpointHostIsolateForm component * Refactor Detections Host isolation flyout to use isolateform
This commit is contained in:
parent
8d85d72fef
commit
532a33b051
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* 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 './isolate_success';
|
||||||
|
export * from './isolate_form';
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* 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, { ChangeEventHandler, memo, ReactNode, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiText,
|
||||||
|
EuiTextArea,
|
||||||
|
EuiTitle,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM } from './translations';
|
||||||
|
|
||||||
|
export interface EndpointIsolatedFormProps {
|
||||||
|
hostName: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onChange: (changes: { comment: string }) => void;
|
||||||
|
comment?: string;
|
||||||
|
/** Any additional message to be appended to the default one */
|
||||||
|
messageAppend?: ReactNode;
|
||||||
|
/** If true, then `Confirm` and `Cancel` buttons will be disabled, and `Confirm` button will loading loading style */
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EndpointIsolateForm = memo<EndpointIsolatedFormProps>(
|
||||||
|
({ hostName, onCancel, onConfirm, onChange, comment = '', messageAppend, isLoading = false }) => {
|
||||||
|
const handleCommentChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||||
|
(event) => {
|
||||||
|
onChange({ comment: event.target.value });
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiText size="s">
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost"
|
||||||
|
defaultMessage="Isolate host {hostName} from network."
|
||||||
|
values={{ hostName: <b>{hostName}</b> }}
|
||||||
|
/>{' '}
|
||||||
|
{messageAppend}
|
||||||
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<h4>{COMMENT}</h4>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiTextArea
|
||||||
|
data-test-subj="host_isolation_comment"
|
||||||
|
fullWidth
|
||||||
|
placeholder={COMMENT_PLACEHOLDER}
|
||||||
|
value={comment}
|
||||||
|
onChange={handleCommentChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
|
<EuiFlexGroup justifyContent="flexEnd">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty onClick={onCancel} disabled={isLoading}>
|
||||||
|
{CANCEL}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton fill onClick={onConfirm} disabled={isLoading} isLoading={isLoading}>
|
||||||
|
{CONFIRM}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
EndpointIsolateForm.displayName = 'EndpointIsolateForm';
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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, { memo, ReactNode } from 'react';
|
||||||
|
import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||||
|
import { GET_SUCCESS_MESSAGE } from './translations';
|
||||||
|
|
||||||
|
export interface EndpointIsolateSuccessProps {
|
||||||
|
hostName: string;
|
||||||
|
completeButtonLabel: string;
|
||||||
|
onComplete: () => void;
|
||||||
|
additionalInfo?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EndpointIsolateSuccess = memo<EndpointIsolateSuccessProps>(
|
||||||
|
({ hostName, onComplete, completeButtonLabel, additionalInfo }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiCallOut iconType="check" color="success" title={GET_SUCCESS_MESSAGE(hostName)}>
|
||||||
|
{additionalInfo}
|
||||||
|
</EuiCallOut>
|
||||||
|
|
||||||
|
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty flush="right" onClick={onComplete}>
|
||||||
|
<EuiText size="s">
|
||||||
|
<p>{completeButtonLabel}</p>
|
||||||
|
</EuiText>
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
EndpointIsolateSuccess.displayName = 'EndpointIsolateSuccess';
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 CANCEL = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.cancel', {
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.confirm', {
|
||||||
|
defaultMessage: 'Confirm',
|
||||||
|
});
|
||||||
|
|
||||||
|
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 GET_SUCCESS_MESSAGE = (hostName: string) =>
|
||||||
|
i18n.translate('xpack.securitySolution.endpoint.hostIsolation.successfulMessage', {
|
||||||
|
defaultMessage: 'Host Isolation on {hostName} successfully submitted',
|
||||||
|
values: { hostName },
|
||||||
|
});
|
|
@ -7,32 +7,19 @@
|
||||||
|
|
||||||
import React, { useMemo, useState, useCallback } from 'react';
|
import React, { useMemo, useState, useCallback } from 'react';
|
||||||
import { find } from 'lodash/fp';
|
import { find } from 'lodash/fp';
|
||||||
import {
|
import { EuiText, EuiSpacer } from '@elastic/eui';
|
||||||
EuiCallOut,
|
|
||||||
EuiTitle,
|
|
||||||
EuiText,
|
|
||||||
EuiTextArea,
|
|
||||||
EuiSpacer,
|
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
EuiButton,
|
|
||||||
EuiButtonEmpty,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
|
import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
|
||||||
import {
|
import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations';
|
||||||
CANCEL,
|
|
||||||
CASES_ASSOCIATED_WITH_ALERT,
|
|
||||||
COMMENT,
|
|
||||||
COMMENT_PLACEHOLDER,
|
|
||||||
CONFIRM,
|
|
||||||
RETURN_TO_ALERT_DETAILS,
|
|
||||||
} from './translations';
|
|
||||||
import { Maybe } from '../../../../../observability/common/typings';
|
import { Maybe } from '../../../../../observability/common/typings';
|
||||||
import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts';
|
import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts';
|
||||||
import { CaseDetailsLink } from '../../../common/components/links';
|
import { CaseDetailsLink } from '../../../common/components/links';
|
||||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||||
|
import {
|
||||||
|
EndpointIsolatedFormProps,
|
||||||
|
EndpointIsolateForm,
|
||||||
|
EndpointIsolateSuccess,
|
||||||
|
} from '../../../common/components/endpoint/host_isolation';
|
||||||
|
|
||||||
export const HostIsolationPanel = React.memo(
|
export const HostIsolationPanel = React.memo(
|
||||||
({
|
({
|
||||||
|
@ -76,6 +63,11 @@ export const HostIsolationPanel = React.memo(
|
||||||
|
|
||||||
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
||||||
|
|
||||||
|
const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback(
|
||||||
|
({ comment: newComment }) => setComment(newComment),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const casesList = useMemo(
|
const casesList = useMemo(
|
||||||
() =>
|
() =>
|
||||||
caseIds.map((id, index) => {
|
caseIds.map((id, index) => {
|
||||||
|
@ -100,43 +92,29 @@ export const HostIsolationPanel = React.memo(
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiCallOut
|
<EndpointIsolateSuccess
|
||||||
iconType="check"
|
hostName={hostName}
|
||||||
color="success"
|
completeButtonLabel={RETURN_TO_ALERT_DETAILS}
|
||||||
title={i18n.translate(
|
onComplete={backToAlertDetails}
|
||||||
'xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.title',
|
additionalInfo={
|
||||||
{
|
caseCount > 0 && (
|
||||||
defaultMessage: 'Host Isolation on {hostname} successfully submitted',
|
<>
|
||||||
values: { hostname: hostName },
|
<EuiText size="s">
|
||||||
}
|
<p>
|
||||||
)}
|
<FormattedMessage
|
||||||
>
|
id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases"
|
||||||
{caseCount > 0 && (
|
defaultMessage="This action has been attached to the following {caseCount, plural, one {case} other {cases}}:"
|
||||||
<>
|
values={{ caseCount }}
|
||||||
<EuiText size="s">
|
/>
|
||||||
<p>
|
</p>
|
||||||
<FormattedMessage
|
</EuiText>
|
||||||
id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases"
|
<EuiText size="s">
|
||||||
defaultMessage="This action has been attached to the following {caseCount, plural, one {case} other {cases}}:"
|
<ul>{casesList}</ul>
|
||||||
values={{ caseCount }}
|
</EuiText>
|
||||||
/>
|
</>
|
||||||
</p>
|
)
|
||||||
</EuiText>
|
}
|
||||||
<EuiText size="s">
|
/>
|
||||||
<ul>{casesList}</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, caseCount, casesList]);
|
}, [backToAlertDetails, hostName, caseCount, casesList]);
|
||||||
|
@ -145,13 +123,18 @@ export const HostIsolationPanel = React.memo(
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<EuiText size="s">
|
<EndpointIsolateForm
|
||||||
<p>
|
hostName={hostName}
|
||||||
|
onCancel={backToAlertDetails}
|
||||||
|
onConfirm={confirmHostIsolation}
|
||||||
|
onChange={handleIsolateFormChange}
|
||||||
|
comment={comment}
|
||||||
|
isLoading={loading}
|
||||||
|
messageAppend={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost"
|
id="xpack.securitySolution.detections.hostIsolation.impactedCases"
|
||||||
defaultMessage="Isolate host {hostname} from network. This action will be added to the {cases}."
|
defaultMessage="This action will be added to the {cases}."
|
||||||
values={{
|
values={{
|
||||||
hostname: <b>{hostName}</b>,
|
|
||||||
cases: (
|
cases: (
|
||||||
<b>
|
<b>
|
||||||
{caseCount}
|
{caseCount}
|
||||||
|
@ -161,42 +144,19 @@ export const HostIsolationPanel = React.memo(
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</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,
|
hostName,
|
||||||
|
backToAlertDetails,
|
||||||
|
confirmHostIsolation,
|
||||||
|
handleIsolateFormChange,
|
||||||
|
comment,
|
||||||
loading,
|
loading,
|
||||||
caseCount,
|
caseCount,
|
||||||
|
alertRule,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return isIsolated ? hostIsolated : hostNotIsolated;
|
return isIsolated ? hostIsolated : hostNotIsolated;
|
||||||
|
|
|
@ -14,23 +14,6 @@ export const ISOLATE_HOST = i18n.translate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
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 = (caseCount: number): string =>
|
export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string =>
|
||||||
i18n.translate(
|
i18n.translate(
|
||||||
'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert',
|
'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert',
|
||||||
|
|
Loading…
Reference in a new issue