[Security Solution] expandable flyout - add isolate host panel (#165933)

## Summary

This new expandable flyout is going GA in `8.10`. One feature isn't
working: the `isolate host` from the `take action` button in the right
section footer. The code was added in this
[PR](https://github.com/elastic/kibana/pull/153903) but isolate host
testing must have been overlooked.

This PR adds the functionality to the new expandable flyout, by creating
a new panel, displayed similarly to the right panel is today.



abd99323-616b-4474-a21c-29ce3c56dd1a

https://github.com/elastic/kibana/pull/165933

### TODO

- [ ] verify logic
- [ ] add unit tests
- [ ] add Cypress tests

### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Ashokaditya <ashokaditya@elastic.co>
This commit is contained in:
Philippe Oberti 2023-09-07 16:49:10 +02:00 committed by GitHub
parent 3f18975c04
commit ed48990395
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 322 additions and 9 deletions

View file

@ -11,6 +11,9 @@ import {
type ExpandableFlyoutProps,
ExpandableFlyoutProvider,
} from '@kbn/expandable-flyout';
import type { IsolateHostPanelProps } from './isolate_host';
import { IsolateHostPanel, IsolateHostPanelKey } from './isolate_host';
import { IsolateHostPanelProvider } from './isolate_host/context';
import type { RightPanelProps } from './right';
import { RightPanel, RightPanelKey } from './right';
import { RightPanelProvider } from './right/context';
@ -54,6 +57,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
</PreviewPanelProvider>
),
},
{
key: IsolateHostPanelKey,
component: (props) => (
<IsolateHostPanelProvider {...(props as IsolateHostPanelProps).params}>
<IsolateHostPanel path={props.path as IsolateHostPanelProps['path']} />
</IsolateHostPanelProvider>
),
},
];
const OuterProviders: FC = ({ children }) => {

View file

@ -0,0 +1,62 @@
/*
* 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 type { FC } from 'react';
import React, { useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { EuiPanel } from '@elastic/eui';
import { RightPanelKey } from '../right';
import { useBasicDataFromDetailsData } from '../../timelines/components/side_panel/event_details/helpers';
import { EndpointIsolateSuccess } from '../../common/components/endpoint/host_isolation';
import { useHostIsolationTools } from '../../timelines/components/side_panel/event_details/use_host_isolation_tools';
import { useIsolateHostPanelContext } from './context';
import { HostIsolationPanel } from '../../detections/components/host_isolation';
/**
* Document details expandable flyout section content for the isolate host component, displaying the form or the success banner
*/
export const PanelContent: FC = () => {
const { openRightPanel } = useExpandableFlyoutContext();
const { dataFormattedForFieldBrowser, eventId, scopeId, indexName, isolateAction } =
useIsolateHostPanelContext();
const { isIsolateActionSuccessBannerVisible, handleIsolationActionSuccess } =
useHostIsolationTools();
const { alertId, hostName } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const showAlertDetails = useCallback(
() =>
openRightPanel({
id: RightPanelKey,
params: {
id: eventId,
indexName,
scopeId,
},
}),
[eventId, indexName, scopeId, openRightPanel]
);
return (
<EuiPanel hasShadow={false} hasBorder={false}>
{isIsolateActionSuccessBannerVisible && (
<EndpointIsolateSuccess
hostName={hostName}
alertId={alertId}
isolateAction={isolateAction}
/>
)}
<HostIsolationPanel
details={dataFormattedForFieldBrowser}
cancelCallback={showAlertDetails}
successCallback={handleIsolationActionSuccess}
isolateAction={isolateAction}
/>
</EuiPanel>
);
};

View file

@ -0,0 +1,122 @@
/*
* 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 type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { css } from '@emotion/react';
import React, { createContext, memo, useContext, useMemo } from 'react';
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { useTimelineEventsDetails } from '../../timelines/containers/details';
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
import { useSpaceId } from '../../common/hooks/use_space_id';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { SecurityPageName } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import type { IsolateHostPanelProps } from '.';
export interface IsolateHostPanelContext {
/**
* Id of the document
*/
eventId: string;
/**
* Name of the index used in the parent's page
*/
indexName: string;
/**
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* An array of field objects with category and value
*/
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
/**
* Isolate action, either 'isolateHost' or 'unisolateHost'
*/
isolateAction: 'isolateHost' | 'unisolateHost';
}
export const IsolateHostPanelContext = createContext<IsolateHostPanelContext | undefined>(
undefined
);
export type IsolateHostPanelProviderProps = {
/**
* React components to render
*/
children: React.ReactNode;
} & Partial<IsolateHostPanelProps['params']>;
export const IsolateHostPanelProvider = memo(
({ id, indexName, scopeId, isolateAction, children }: IsolateHostPanelProviderProps) => {
const currentSpaceId = useSpaceId();
// TODO Replace getAlertIndexAlias way to retrieving the eventIndex with the GET /_alias
// https://github.com/elastic/kibana/issues/113063
const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : '';
const [{ pageName }] = useRouteSpy();
const sourcererScope =
pageName === SecurityPageName.detections
? SourcererScopeName.detections
: SourcererScopeName.default;
const sourcererDataView = useSourcererDataView(sourcererScope);
const [loading, dataFormattedForFieldBrowser] = useTimelineEventsDetails({
indexName: eventIndex,
eventId: id ?? '',
runtimeMappings: sourcererDataView.runtimeMappings,
skip: !id,
});
const contextValue = useMemo(
() =>
id && indexName && scopeId && isolateAction
? {
eventId: id,
indexName,
scopeId,
dataFormattedForFieldBrowser,
isolateAction,
}
: undefined,
[id, indexName, scopeId, dataFormattedForFieldBrowser, isolateAction]
);
if (loading) {
return (
<EuiFlexItem
css={css`
align-items: center;
justify-content: center;
`}
>
<EuiLoadingSpinner size="xxl" />
</EuiFlexItem>
);
}
return (
<IsolateHostPanelContext.Provider value={contextValue}>
{children}
</IsolateHostPanelContext.Provider>
);
}
);
IsolateHostPanelProvider.displayName = 'IsolateHostPanelProvider';
export const useIsolateHostPanelContext = (): IsolateHostPanelContext => {
const contextValue = useContext(IsolateHostPanelContext);
if (!contextValue) {
throw new Error(
'IsolateHostPanelContext can only be used within IsolateHostPanelContext provider'
);
}
return contextValue;
};

View file

@ -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 { EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
import type { FC } from 'react';
import React from 'react';
import { useIsolateHostPanelContext } from './context';
import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
import { PANEL_HEADER_ISOLATE_TITLE, PANEL_HEADER_RELEASE_TITLE } from './translations';
/**
* Document details expandable right section header for the isolate host panel
*/
export const PanelHeader: FC = () => {
const { isolateAction } = useIsolateHostPanelContext();
const title =
isolateAction === 'isolateHost' ? PANEL_HEADER_ISOLATE_TITLE : PANEL_HEADER_RELEASE_TITLE;
return (
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h4 data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>{title}</h4>
</EuiTitle>
</EuiFlyoutHeader>
);
};

View file

@ -0,0 +1,37 @@
/*
* 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 type { FC } from 'react';
import React from 'react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { PanelContent } from './content';
import { PanelHeader } from './header';
export const IsolateHostPanelKey: IsolateHostPanelProps['key'] = 'document-details-isolate-host';
export interface IsolateHostPanelProps extends FlyoutPanelProps {
key: 'document-details-isolate-host';
params?: {
id: string;
indexName: string;
scopeId: string;
isolateAction: 'isolateHost' | 'unisolateHost' | undefined;
};
}
/**
* Panel to be displayed right section in the document details expandable flyout when isolate host is clicked in the
* take action button
*/
export const IsolateHostPanel: FC<Partial<IsolateHostPanelProps>> = () => {
return (
<>
<PanelHeader />
<PanelContent />
</>
);
};

View file

@ -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 const FLYOUT_HEADER_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutHeaderTitle';

View file

@ -0,0 +1,22 @@
/*
* 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 PANEL_HEADER_ISOLATE_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.isolateHostPanelHeaderIsolateTitle',
{
defaultMessage: `Isolate host`,
}
);
export const PANEL_HEADER_RELEASE_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.isolateHostPanelHeaderReleaseTitle',
{
defaultMessage: `Release host`,
}
);

View file

@ -6,7 +6,7 @@
*/
import type { FC } from 'react';
import React, { memo } from 'react';
import React, { useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FlyoutFooter } from '../../timelines/components/side_panel/event_details/flyout';
import { useRightPanelContext } from './context';
@ -15,13 +15,35 @@ import { useHostIsolationTools } from '../../timelines/components/side_panel/eve
/**
*
*/
export const PanelFooter: FC = memo(() => {
const { closeFlyout } = useExpandableFlyoutContext();
const { dataFormattedForFieldBrowser, dataAsNestedObject, refetchFlyoutData, scopeId } =
useRightPanelContext();
export const PanelFooter: FC = () => {
const { closeFlyout, openRightPanel } = useExpandableFlyoutContext();
const {
eventId,
indexName,
dataFormattedForFieldBrowser,
dataAsNestedObject,
refetchFlyoutData,
scopeId,
} = useRightPanelContext();
const { isHostIsolationPanelOpen, showHostIsolationPanel } = useHostIsolationTools();
const showHostIsolationPanelCallback = useCallback(
(action: 'isolateHost' | 'unisolateHost' | undefined) => {
showHostIsolationPanel(action);
openRightPanel({
id: 'document-details-isolate-host',
params: {
id: eventId,
indexName,
scopeId,
isolateAction: action,
},
});
},
[eventId, indexName, openRightPanel, scopeId, showHostIsolationPanel]
);
if (!dataFormattedForFieldBrowser || !dataAsNestedObject) {
return null;
}
@ -34,11 +56,9 @@ export const PanelFooter: FC = memo(() => {
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
isReadOnly={false}
loadingEventDetails={false}
onAddIsolationStatusClick={showHostIsolationPanel}
onAddIsolationStatusClick={showHostIsolationPanelCallback}
scopeId={scopeId}
refetchFlyoutData={refetchFlyoutData}
/>
);
});
PanelFooter.displayName = 'PanelFooter';
};