Skip to content

Commit

Permalink
[Security Solution] add session view component to expandable flyout (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeOberti authored Apr 21, 2023
1 parent dbb8e2e commit ec6bc2d
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ describe.skip('Alert details expandable flyout left panel', { testIsolation: fal
it('should display content when switching buttons', () => {
openVisualizeTab();
openSessionView();
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT)
.should('be.visible')
.and('have.text', 'Session view');
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT).should('be.visible');

openGraphAnalyzer();
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT).should('be.visible');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA } from '../../../screens/document_expandable_flyout';
import {
expandFirstAlertExpandableFlyout,
expandDocumentDetailsExpandableFlyoutLeftSection,
} from '../../../tasks/document_expandable_flyout';
import { cleanKibana } from '../../../tasks/common';
import { login, visit } from '../../../tasks/login';
import { createRule } from '../../../tasks/api_calls/rules';
import { getNewRule } from '../../../objects/rule';
import { ALERTS_URL } from '../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';

// Skipping these for now as the feature is protected behind a feature flag set to false by default
// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50
describe.skip(
'Alert details expandable flyout left panel session view',
{ testIsolation: false },
() => {
before(() => {
cleanKibana();
login();
createRule(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlertExpandableFlyout();
expandDocumentDetailsExpandableFlyoutLeftSection();
});

it('should display session view no data message', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA)
.should('be.visible')
.and('contain.text', 'No data to render')
.and('contain.text', 'No process events found for this query');
});

it('should display session view component', () => {});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON = getData
);
export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT =
getDataTestSubjectSelector(SESSION_VIEW_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA =
getDataTestSubjectSelector('sessionView:sessionViewProcessEventsEmpty');
export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON =
getDataTestSubjectSelector(VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT =
Expand Down
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import type { Story } from '@storybook/react';
import { SessionView } from './session_view';
import type { LeftPanelContext } from '../context';
import { LeftFlyoutContext } from '../context';

export default {
component: SessionView,
title: 'Flyout/SessionView',
};

// TODO to get this working, we need to spent some time getting all the foundation items for storybook
// (ReduxStoreProvider, CellActionsProvider...) similarly to how it was done for the TestProvidersComponent
// see ticket https://github.com/elastic/security-team/issues/6223
// export const Default: Story<void> = () => {
// const contextValue = {
// getFieldsData: () => {},
// } as unknown as LeftPanelContext;
//
// return (
// <LeftFlyoutContext.Provider value={contextValue}>
// <SessionView />
// </LeftFlyoutContext.Provider>
// );
// };

export const Error: Story<void> = () => {
const contextValue = {
getFieldsData: () => {},
} as unknown as LeftPanelContext;

return (
<LeftFlyoutContext.Provider value={contextValue}>
<SessionView />
</LeftFlyoutContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import type { LeftPanelContext } from '../context';
import { LeftFlyoutContext } from '../context';
import { TestProviders } from '../../../common/mock';
import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids';
import { SessionView } from './session_view';

jest.mock('../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../common/lib/kibana');
return {
...originalModule,
useKibana: jest.fn().mockReturnValue({
services: {
sessionView: {
getSessionView: jest.fn().mockReturnValue(<div />),
},
},
}),
};
});

describe('<SessionView />', () => {
it('renders session view correctly', () => {
const contextValue = {
getFieldsData: () => 'id',
} as unknown as LeftPanelContext;

const wrapper = render(
<TestProviders>
<LeftFlyoutContext.Provider value={contextValue}>
<SessionView />
</LeftFlyoutContext.Provider>
</TestProviders>
);
expect(wrapper.getByTestId(SESSION_VIEW_TEST_ID)).toBeInTheDocument();
});

it('should render error message on null eventId', () => {
const contextValue = {
getFieldsData: () => {},
} as unknown as LeftPanelContext;

const wrapper = render(
<TestProviders>
<LeftFlyoutContext.Provider value={contextValue}>
<SessionView />
</LeftFlyoutContext.Provider>
</TestProviders>
);
expect(wrapper.getByTestId(SESSION_VIEW_ERROR_TEST_ID)).toBeInTheDocument();
expect(wrapper.getByText('Unable to display session view')).toBeInTheDocument();
expect(wrapper.getByText('There was an error displaying session view')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,46 @@

import type { FC } from 'react';
import React from 'react';
import { EuiText } from '@elastic/eui';
import { SESSION_VIEW_TEST_ID } from './test_ids';
import { EuiEmptyPrompt } from '@elastic/eui';
import { getField } from '../../shared/utils';
import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations';
import { SESSION_VIEW_ERROR_MESSAGE } from './translations';
import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids';
import { useKibana } from '../../../common/lib/kibana';
import { useLeftPanelContext } from '../context';

export const SESSION_VIEW_ID = 'session_view';
const SESSION_ENTITY_ID = 'process.entry_leader.entity_id';

/**
* Session view displayed in the document details expandable flyout left section under the Visualize tab
*/
export const SessionView: FC = () => {
return <EuiText data-test-subj={SESSION_VIEW_TEST_ID}>{'Session view'}</EuiText>;
const { sessionView } = useKibana().services;
const { getFieldsData } = useLeftPanelContext();

const sessionEntityId = getField(getFieldsData(SESSION_ENTITY_ID));

if (!sessionEntityId) {
return (
<EuiEmptyPrompt
iconType="error"
color="danger"
title={<h2>{ERROR_TITLE(SESSION_VIEW_ERROR_MESSAGE)}</h2>}
body={<p>{ERROR_MESSAGE(SESSION_VIEW_ERROR_MESSAGE)}</p>}
data-test-subj={SESSION_VIEW_ERROR_TEST_ID}
/>
);
}

return (
<div data-test-subj={SESSION_VIEW_TEST_ID}>
{sessionView.getSessionView({
sessionEntityId,
isFullScreen: true,
})}
</div>
);
};

SessionView.displayName = 'SessionView';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const ANALYZER_GRAPH_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnal
export const ANALYZE_GRAPH_ERROR_TEST_ID =
'securitySolutionDocumentDetailsFlyoutAnalyzerGraphError';
export const SESSION_VIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionView';
export const SESSION_VIEW_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionViewError';
export const ENTITIES_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesDetails';
export const THREAT_INTELLIGENCE_DETAILS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutThreatIntelligenceDetails';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ export const ANALYZER_ERROR_MESSAGE = i18n.translate(
defaultMessage: 'analyzer',
}
);

export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate(
'xpack.securitySolution.flyout.sessionViewErrorTitle',
{
defaultMessage: 'session view',
}
);
47 changes: 45 additions & 2 deletions x-pack/plugins/security_solution/public/flyout/left/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
*/

import React, { createContext, useContext, useMemo } from 'react';
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { css } from '@emotion/react';
import { SecurityPageName } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import { useTimelineEventsDetails } from '../../timelines/containers/details';
import { useGetFieldsData } from '../../common/hooks/use_get_fields_data';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { useSpaceId } from '../../common/hooks/use_space_id';
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
import type { LeftPanelProps } from '.';

export interface LeftPanelContext {
Expand All @@ -17,6 +27,10 @@ export interface LeftPanelContext {
* Name of the index used in the parent's page
*/
indexName: string;
/**
* Retrieves searchHit values for the provided field
*/
getFieldsData: (field: string) => unknown | unknown[];
}

export const LeftFlyoutContext = createContext<LeftPanelContext | undefined>(undefined);
Expand All @@ -29,11 +43,40 @@ export type LeftPanelProviderProps = {
} & Partial<LeftPanelProps['params']>;

export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProviderProps) => {
const currentSpaceId = useSpaceId();
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, _, searchHit] = useTimelineEventsDetails({
indexName: eventIndex,
eventId: id ?? '',
runtimeMappings: sourcererDataView.runtimeMappings,
skip: !id,
});
const getFieldsData = useGetFieldsData(searchHit?.fields);

const contextValue = useMemo(
() => (id && indexName ? { eventId: id, indexName } : undefined),
[id, indexName]
() => (id && indexName ? { eventId: id, indexName, getFieldsData } : undefined),
[id, indexName, getFieldsData]
);

if (loading) {
return (
<EuiFlexItem
css={css`
align-items: center;
justify-content: center;
`}
>
<EuiLoadingSpinner size="xxl" />
</EuiFlexItem>
);
}

return <LeftFlyoutContext.Provider value={contextValue}>{children}</LeftFlyoutContext.Provider>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SecurityPageName } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import type { RightPanelProps } from '.';
import type { GetFieldsData } from '../../common/hooks/use_get_fields_data';
import { useGetFieldsData } from '../../common/hooks/use_get_fields_data';

export interface RightPanelContext {
Expand Down Expand Up @@ -57,7 +58,7 @@ export interface RightPanelContext {
/**
* Retrieves searchHit values for the provided field
*/
getFieldsData: (field: string) => unknown | unknown[];
getFieldsData: GetFieldsData;
}

export const RightPanelContext = createContext<RightPanelContext | undefined>(undefined);
Expand Down

0 comments on commit ec6bc2d

Please sign in to comment.