Skip to content

Commit

Permalink
[Security Solution] expandable flyout - add isolate host panel (#165933)
Browse files Browse the repository at this point in the history
## 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](#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.



https://github.com/elastic/kibana/assets/17276605/abd99323-616b-4474-a21c-29ce3c56dd1a

#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>
  • Loading branch information
PhilippeOberti and ashokaditya authored Sep 7, 2023
1 parent 3f18975 commit ed48990
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 9 deletions.
11 changes: 11 additions & 0 deletions x-pack/plugins/security_solution/public/flyout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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 />
</>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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`,
}
);
38 changes: 29 additions & 9 deletions x-pack/plugins/security_solution/public/flyout/right/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand All @@ -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';
};

0 comments on commit ed48990

Please sign in to comment.