mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.10] [Security Solution] expandable flyout - hide visualize tab in left section and open session view and analyzer in timeline (#164111) (#164735)
# Backport This will backport the following commits from `main` to `8.10`: - [[Security Solution] expandable flyout - hide visualize tab in left section and open session view and analyzer in timeline (#164111)](https://github.com/elastic/kibana/pull/164111) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Philippe Oberti","email":"philippe.oberti@elastic.co"},"sourceCommit":{"committedDate":"2023-08-24T15:11:00Z","message":"[Security Solution] expandable flyout - hide visualize tab in left section and open session view and analyzer in timeline (#164111)","sha":"acedd23097f2476629cb14dce7da6ee46ee237df","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Threat Hunting:Investigations","v8.10.0","v8.11.0"],"number":164111,"url":"https://github.com/elastic/kibana/pull/164111","mergeCommit":{"message":"[Security Solution] expandable flyout - hide visualize tab in left section and open session view and analyzer in timeline (#164111)","sha":"acedd23097f2476629cb14dce7da6ee46ee237df"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164111","number":164111,"mergeCommit":{"message":"[Security Solution] expandable flyout - hide visualize tab in left section and open session view and analyzer in timeline (#164111)","sha":"acedd23097f2476629cb14dce7da6ee46ee237df"}}]}] BACKPORT--> Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
parent
017fda6497
commit
070b357a67
30 changed files with 684 additions and 539 deletions
|
@ -23,6 +23,7 @@ import {
|
|||
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID,
|
||||
CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID,
|
||||
CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID,
|
||||
CORRELATIONS_DETAILS_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session';
|
||||
import { useFetchRelatedAlertsByAncestry } from '../../shared/hooks/use_fetch_related_alerts_by_ancestry';
|
||||
|
@ -103,7 +104,7 @@ describe('CorrelationsDetails', () => {
|
|||
expect(getByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no section if show values are false', () => {
|
||||
it('should render no section and show error message if show values are false', () => {
|
||||
jest
|
||||
.mocked(useShowRelatedAlertsByAncestry)
|
||||
.mockReturnValue({ show: false, documentId: 'documentId', indices: ['index1'] });
|
||||
|
@ -115,12 +116,13 @@ describe('CorrelationsDetails', () => {
|
|||
.mockReturnValue({ show: false, entityId: 'entityId' });
|
||||
jest.mocked(useShowRelatedCases).mockReturnValue(false);
|
||||
|
||||
const { queryByTestId } = renderCorrelationDetails();
|
||||
const { getByTestId, queryByTestId } = renderCorrelationDetails();
|
||||
|
||||
expect(queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(`${CORRELATIONS_DETAILS_TEST_ID}Error`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no section if values are null', () => {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { CORRELATIONS_ERROR_MESSAGE } from './translations';
|
||||
import { CORRELATIONS_DETAILS_TEST_ID } from './test_ids';
|
||||
import { RelatedAlertsBySession } from './related_alerts_by_session';
|
||||
import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_event';
|
||||
import { RelatedCases } from './related_cases';
|
||||
|
@ -42,27 +44,38 @@ export const CorrelationsDetails: React.FC = () => {
|
|||
const { show: showAlertsBySession, entityId } = useShowRelatedAlertsBySession({ getFieldsData });
|
||||
const showCases = useShowRelatedCases();
|
||||
|
||||
const canShowAtLeastOneInsight =
|
||||
showAlertsByAncestry || showSameSourceAlerts || showAlertsBySession || showCases;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAlertsByAncestry && documentId && indices && (
|
||||
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
|
||||
{canShowAtLeastOneInsight ? (
|
||||
<>
|
||||
{showAlertsByAncestry && documentId && indices && (
|
||||
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{showSameSourceAlerts && originalEventId && (
|
||||
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{showAlertsBySession && entityId && (
|
||||
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{showCases && <RelatedCases eventId={eventId} />}
|
||||
</>
|
||||
) : (
|
||||
<div data-test-subj={`${CORRELATIONS_DETAILS_TEST_ID}Error`}>
|
||||
{CORRELATIONS_ERROR_MESSAGE}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{showSameSourceAlerts && originalEventId && (
|
||||
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{showAlertsBySession && entityId && (
|
||||
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{showCases && <RelatedCases eventId={eventId} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,6 +21,13 @@ export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CORRELATIONS_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.correlationsErrorMessage',
|
||||
{
|
||||
defaultMessage: 'No correlations data available',
|
||||
}
|
||||
);
|
||||
|
||||
export const USER_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.userTitle', {
|
||||
defaultMessage: 'User',
|
||||
});
|
||||
|
|
|
@ -20,11 +20,11 @@ export interface PanelContentProps {
|
|||
|
||||
/**
|
||||
* Document details expandable flyout left section. Appears after the user clicks on the expand details button in the right section.
|
||||
* Will display the content of the visualize, investigation and insights tabs.
|
||||
* Displays the content of investigation and insights tabs (visualize is hidden for 8.9).
|
||||
*/
|
||||
export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId }) => {
|
||||
const selectedTabContent = useMemo(() => {
|
||||
return tabs.find((tab) => tab.id === selectedTabId)?.content;
|
||||
return tabs.filter((tab) => tab.visible).find((tab) => tab.id === selectedTabId)?.content;
|
||||
}, [selectedTabId]);
|
||||
|
||||
return <EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>;
|
||||
|
|
|
@ -13,15 +13,26 @@ import type { LeftPanelPaths } from '.';
|
|||
import { tabs } from './tabs';
|
||||
|
||||
export interface PanelHeaderProps {
|
||||
/**
|
||||
* Id of the tab selected in the parent component to display its content
|
||||
*/
|
||||
selectedTabId: LeftPanelPaths;
|
||||
/**
|
||||
* Callback to set the selected tab id in the parent component
|
||||
* @param selected
|
||||
*/
|
||||
setSelectedTabId: (selected: LeftPanelPaths) => void;
|
||||
handleOnEventClosed?: () => void;
|
||||
}
|
||||
|
||||
export const PanelHeader: VFC<PanelHeaderProps> = memo(
|
||||
({ selectedTabId, setSelectedTabId, handleOnEventClosed }) => {
|
||||
const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id);
|
||||
const renderTabs = tabs.map((tab, index) => (
|
||||
/**
|
||||
* Header at the top of the left section.
|
||||
* Displays the investigation and insights tabs (visualize is hidden for 8.9).
|
||||
*/
|
||||
export const PanelHeader: VFC<PanelHeaderProps> = memo(({ selectedTabId, setSelectedTabId }) => {
|
||||
const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id);
|
||||
const renderTabs = tabs
|
||||
.filter((tab) => tab.visible)
|
||||
.map((tab, index) => (
|
||||
<EuiTab
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
|
@ -32,20 +43,19 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo(
|
|||
</EuiTab>
|
||||
));
|
||||
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTabs
|
||||
size="l"
|
||||
expand
|
||||
css={css`
|
||||
margin-bottom: -25px;
|
||||
`}
|
||||
>
|
||||
{renderTabs}
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTabs
|
||||
size="l"
|
||||
expand
|
||||
css={css`
|
||||
margin-bottom: -25px;
|
||||
`}
|
||||
>
|
||||
{renderTabs}
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
);
|
||||
});
|
||||
|
||||
PanelHeader.displayName = 'PanelHeader';
|
||||
|
|
|
@ -39,9 +39,10 @@ export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {
|
|||
const { eventId, indexName, scopeId } = useLeftPanelContext();
|
||||
|
||||
const selectedTabId = useMemo(() => {
|
||||
const defaultTab = tabs[0].id;
|
||||
const visibleTabs = tabs.filter((tab) => tab.visible);
|
||||
const defaultTab = visibleTabs[0].id;
|
||||
if (!path) return defaultTab;
|
||||
return tabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
|
||||
return visibleTabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
|
||||
}, [path]);
|
||||
|
||||
const setSelectedTabId = (tabId: LeftPanelTabsType[number]['id']) => {
|
||||
|
|
|
@ -24,6 +24,7 @@ export type LeftPanelTabsType = Array<{
|
|||
'data-test-subj': string;
|
||||
name: string;
|
||||
content: React.ReactElement;
|
||||
visible: boolean;
|
||||
}>;
|
||||
|
||||
export const tabs: LeftPanelTabsType = [
|
||||
|
@ -32,23 +33,27 @@ export const tabs: LeftPanelTabsType = [
|
|||
'data-test-subj': VISUALIZE_TAB_TEST_ID,
|
||||
name: VISUALIZE_TAB,
|
||||
content: <VisualizeTab />,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
id: 'insights',
|
||||
'data-test-subj': INSIGHTS_TAB_TEST_ID,
|
||||
name: INSIGHTS_TAB,
|
||||
content: <InsightsTab />,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'investigation',
|
||||
'data-test-subj': INVESTIGATION_TAB_TEST_ID,
|
||||
name: INVESTIGATIONS_TAB,
|
||||
content: <InvestigationTab />,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'response',
|
||||
'data-test-subj': RESPONSE_TAB_TEST_ID,
|
||||
name: RESPONSE_TAB,
|
||||
content: <ResponseTab />,
|
||||
visible: true,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -15,7 +15,6 @@ import { RightPanelContext } from '../context';
|
|||
import { AnalyzerPreview } from './analyzer_preview';
|
||||
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
|
||||
import * as mock from '../mocks/mock_analyzer_data';
|
||||
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '../../shared/components/test_ids';
|
||||
|
||||
jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({
|
||||
useAlertPrevalenceFromProcessTree: jest.fn(),
|
||||
|
@ -65,9 +64,7 @@ describe('<AnalyzerPreview />', () => {
|
|||
documentId: 'ancestors-id',
|
||||
indices: ['rule-parameters-index'],
|
||||
});
|
||||
expect(
|
||||
wrapper.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show analyzer preview when documentid and index are not present', () => {
|
||||
|
@ -90,8 +87,6 @@ describe('<AnalyzerPreview />', () => {
|
|||
documentId: '',
|
||||
indices: [],
|
||||
});
|
||||
expect(
|
||||
queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId(ANALYZER_PREVIEW_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { find } from 'lodash/fp';
|
||||
import { EuiTreeView } from '@elastic/eui';
|
||||
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { getTreeNodes } from '../utils/analyzer_helpers';
|
||||
import { ANALYZER_PREVIEW_TITLE } from './translations';
|
||||
import { ANCESTOR_ID, RULE_PARAMETERS_INDEX } from '../../shared/constants/field_names';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
|
||||
import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
|
||||
import { AnalyzerTree } from './analyzer_tree';
|
||||
import { isActiveTimeline } from '../../../helpers';
|
||||
|
||||
const CHILD_COUNT_LIMIT = 3;
|
||||
|
@ -38,7 +41,7 @@ export const AnalyzerPreview: React.FC = () => {
|
|||
const index = find({ category: 'kibana', field: RULE_PARAMETERS_INDEX }, data);
|
||||
const indices = index?.values ?? [];
|
||||
|
||||
const { loading, error, statsNodes } = useAlertPrevalenceFromProcessTree({
|
||||
const { statsNodes } = useAlertPrevalenceFromProcessTree({
|
||||
isActiveTimeline: isActiveTimeline(scopeId),
|
||||
documentId: processDocumentId,
|
||||
indices,
|
||||
|
@ -50,19 +53,24 @@ export const AnalyzerPreview: React.FC = () => {
|
|||
}
|
||||
}, [statsNodes, setCache]);
|
||||
|
||||
if (!documentId || !index) {
|
||||
const items = useMemo(
|
||||
() => getTreeNodes(cache.statsNodes ?? [], CHILD_COUNT_LIMIT, ANCESTOR_LEVEL, DESCENDANT_LEVEL),
|
||||
[cache.statsNodes]
|
||||
);
|
||||
|
||||
if (!documentId || !index || !items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnalyzerTree
|
||||
statsNodes={cache.statsNodes}
|
||||
loading={loading}
|
||||
error={error}
|
||||
childCountLimit={CHILD_COUNT_LIMIT}
|
||||
ancestorLevel={ANCESTOR_LEVEL}
|
||||
descendantLevel={DESCENDANT_LEVEL}
|
||||
/>
|
||||
<div data-test-subj={ANALYZER_PREVIEW_TEST_ID}>
|
||||
<EuiTreeView
|
||||
items={items}
|
||||
display="compressed"
|
||||
aria-label={ANALYZER_PREVIEW_TITLE}
|
||||
showExpansionArrows
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import React from 'react';
|
||||
import { RightPanelContext } from '../context';
|
||||
import { AnalyzerPreviewContainer } from './analyzer_preview_container';
|
||||
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
|
||||
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
|
||||
import * as mock from '../mocks/mock_analyzer_data';
|
||||
import {
|
||||
EXPANDABLE_PANEL_CONTENT_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
|
||||
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
|
||||
} from '../../shared/components/test_ids';
|
||||
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_context';
|
||||
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
|
||||
|
||||
jest.mock('../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver');
|
||||
jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree');
|
||||
jest.mock(
|
||||
'../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'
|
||||
);
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
|
||||
});
|
||||
|
||||
const panelContextValue = {
|
||||
dataAsNestedObject: null,
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const TEST_ID = ANALYZER_PREVIEW_TEST_ID;
|
||||
const ERROR_TEST_ID = `${ANALYZER_PREVIEW_TEST_ID}Error`;
|
||||
|
||||
const renderAnalyzerPreview = () =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<AnalyzerPreviewContainer />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('AnalyzerPreviewContainer', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should render component and link in header', () => {
|
||||
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
|
||||
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
alertIds: ['alertid'],
|
||||
statsNodes: mock.mockStatsNodes,
|
||||
});
|
||||
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
|
||||
investigateInTimelineAlertClick: jest.fn(),
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = renderAnalyzerPreview();
|
||||
|
||||
expect(getByTestId(TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(ERROR_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message and text in header', () => {
|
||||
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(false);
|
||||
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
|
||||
investigateInTimelineAlertClick: jest.fn(),
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = renderAnalyzerPreview();
|
||||
|
||||
expect(queryByTestId(TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(ERROR_TEST_ID)).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to left section Visualize tab when clicking on title', () => {
|
||||
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
|
||||
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
|
||||
loading: false,
|
||||
error: false,
|
||||
alertIds: ['alertid'],
|
||||
statsNodes: mock.mockStatsNodes,
|
||||
});
|
||||
(useInvestigateInTimeline as jest.Mock).mockReturnValue({
|
||||
investigateInTimelineAlertClick: jest.fn(),
|
||||
});
|
||||
|
||||
const { getByTestId } = renderAnalyzerPreview();
|
||||
|
||||
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({});
|
||||
|
||||
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click();
|
||||
expect(investigateInTimelineAlertClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { TimelineTabs } from '@kbn/securitysolution-data-table';
|
||||
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
|
||||
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
|
||||
import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions';
|
||||
import { getScopedActions } from '../../../helpers';
|
||||
import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
|
||||
import { AnalyzerPreview } from './analyzer_preview';
|
||||
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { ANALYZER_PREVIEW_ERROR, ANALYZER_PREVIEW_TITLE } from './translations';
|
||||
import { ExpandablePanel } from '../../shared/components/expandable_panel';
|
||||
|
||||
const timelineId = 'timeline-1';
|
||||
|
||||
/**
|
||||
* Analyzer preview under Overview, Visualizations. It shows a tree representation of analyzer.
|
||||
*/
|
||||
export const AnalyzerPreviewContainer: React.FC = () => {
|
||||
const { dataAsNestedObject } = useRightPanelContext();
|
||||
|
||||
// decide whether to show the analyzer preview or not
|
||||
const isEnabled = isInvestigateInResolverActionEnabled(dataAsNestedObject || undefined);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({
|
||||
ecsRowData: dataAsNestedObject,
|
||||
});
|
||||
|
||||
// open timeline to the analyzer tab because the expandable flyout left panel Visualize => Analyzer tab is not ready
|
||||
const goToAnalyzerTab = useCallback(() => {
|
||||
// open timeline
|
||||
investigateInTimelineAlertClick();
|
||||
|
||||
// open analyzer tab
|
||||
startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER });
|
||||
const scopedActions = getScopedActions(timelineId);
|
||||
if (scopedActions && dataAsNestedObject) {
|
||||
dispatch(
|
||||
scopedActions.updateGraphEventId({ id: timelineId, graphEventId: dataAsNestedObject._id })
|
||||
);
|
||||
}
|
||||
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }));
|
||||
}, [dataAsNestedObject, dispatch, investigateInTimelineAlertClick, startTransaction]);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
title: ANALYZER_PREVIEW_TITLE,
|
||||
iconType: 'timeline',
|
||||
...(isEnabled && { callback: goToAnalyzerTab }),
|
||||
}}
|
||||
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
|
||||
>
|
||||
{isEnabled ? (
|
||||
<AnalyzerPreview />
|
||||
) : (
|
||||
<div data-test-subj={`${ANALYZER_PREVIEW_TEST_ID}Error`}>{ANALYZER_PREVIEW_ERROR}</div>
|
||||
)}
|
||||
</ExpandablePanel>
|
||||
);
|
||||
};
|
||||
|
||||
AnalyzerPreviewContainer.displayName = 'AnalyzerPreviewContainer';
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* 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 { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
|
||||
import { AnalyzerTree } from './analyzer_tree';
|
||||
import * as mock from '../mocks/mock_analyzer_data';
|
||||
import { RightPanelContext } from '../context';
|
||||
|
||||
export default {
|
||||
component: AnalyzerTree,
|
||||
title: 'Flyout/AnalyzerTree',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: () => {},
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
|
||||
const contextValue = {
|
||||
eventId: 'eventId',
|
||||
indexName: 'indexName',
|
||||
scopeId: 'alerts-page',
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const wrapper = (children: React.ReactNode) => (
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={contextValue}>{children}</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
);
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodes} />);
|
||||
};
|
||||
|
||||
export const SingleNode: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={[mock.mockStatsNode]} />);
|
||||
};
|
||||
|
||||
export const ShowParent: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodesHasParent} />);
|
||||
};
|
||||
|
||||
export const ShowGrandparent: Story<void> = () => {
|
||||
return wrapper(
|
||||
<AnalyzerTree
|
||||
{...defaultProps}
|
||||
statsNodes={mock.mockStatsNodesHasGrandparent}
|
||||
ancestorLevel={2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const HideGrandparent: Story<void> = () => {
|
||||
return wrapper(
|
||||
<AnalyzerTree
|
||||
{...defaultProps}
|
||||
statsNodes={mock.mockStatsNodesHasGrandparent}
|
||||
ancestorLevel={1}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShowChildren: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodesHasChildren} />);
|
||||
};
|
||||
|
||||
export const ShowOnlyOneChild: Story<void> = () => {
|
||||
return wrapper(
|
||||
<AnalyzerTree
|
||||
{...defaultProps}
|
||||
statsNodes={mock.mockStatsNodesHasChildren}
|
||||
childCountLimit={1}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShowGrandchildren: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree {...defaultProps} statsNodes={mock.mockStatsNodesHasChildren} />);
|
||||
};
|
||||
|
||||
export const HideGrandchildren: Story<void> = () => {
|
||||
return wrapper(
|
||||
<AnalyzerTree
|
||||
{...defaultProps}
|
||||
statsNodes={mock.mockStatsNodesHasChildren}
|
||||
descendantLevel={1}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Loading: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree loading={true} error={false} descendantLevel={3} />);
|
||||
};
|
||||
|
||||
export const Error: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree loading={false} error={true} descendantLevel={3} />);
|
||||
};
|
||||
|
||||
export const Empty: Story<void> = () => {
|
||||
return wrapper(<AnalyzerTree {...defaultProps} />);
|
||||
};
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* 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 { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { ANALYZE_GRAPH_ID } from '../../left/components/analyze_graph';
|
||||
import { ANALYZER_PREVIEW_TITLE } from './translations';
|
||||
import * as mock from '../mocks/mock_analyzer_data';
|
||||
import type { AnalyzerTreeProps } from './analyzer_tree';
|
||||
import { AnalyzerTree } from './analyzer_tree';
|
||||
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
|
||||
import { TestProviders } from '@kbn/timelines-plugin/public/mock';
|
||||
import { RightPanelContext } from '../context';
|
||||
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
|
||||
|
||||
import {
|
||||
EXPANDABLE_PANEL_CONTENT_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
|
||||
EXPANDABLE_PANEL_LOADING_TEST_ID,
|
||||
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
|
||||
} from '../../shared/components/test_ids';
|
||||
const TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
|
||||
const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
|
||||
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
|
||||
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
|
||||
const CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
|
||||
const LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
|
||||
|
||||
const defaultProps: AnalyzerTreeProps = {
|
||||
statsNodes: mock.mockStatsNodes,
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
|
||||
const panelContextValue = {
|
||||
eventId: 'event id',
|
||||
indexName: 'indexName',
|
||||
browserFields: {},
|
||||
dataFormattedForFieldBrowser: [],
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const renderAnalyzerTree = (children: React.ReactNode) =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
{children}
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('<AnalyzerTree />', () => {
|
||||
it('should render wrapper component', () => {
|
||||
const { getByTestId, queryByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} />);
|
||||
expect(queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_LINK_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(TITLE_ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the component when data is passed', () => {
|
||||
const { getByTestId, getByText } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} />);
|
||||
expect(getByText(ANALYZER_PREVIEW_TITLE)).toBeInTheDocument();
|
||||
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render blank when data is not passed', () => {
|
||||
const { queryByTestId, queryByText } = renderAnalyzerTree(
|
||||
<AnalyzerTree {...defaultProps} statsNodes={undefined} />
|
||||
);
|
||||
expect(queryByText(ANALYZER_PREVIEW_TITLE)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(CONTENT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading spinner when loading is true', () => {
|
||||
const { getByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} loading={true} />);
|
||||
expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render when error is true', () => {
|
||||
const { getByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} error={true} />);
|
||||
expect(getByTestId(CONTENT_TEST_ID)).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should navigate to left section Visualize tab when clicking on title', () => {
|
||||
const { getByTestId } = renderAnalyzerTree(<AnalyzerTree {...defaultProps} />);
|
||||
|
||||
getByTestId(TITLE_LINK_TEST_ID).click();
|
||||
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
|
||||
id: LeftPanelKey,
|
||||
path: { tab: LeftPanelVisualizeTab, subTab: ANALYZE_GRAPH_ID },
|
||||
params: {
|
||||
id: panelContextValue.eventId,
|
||||
indexName: panelContextValue.indexName,
|
||||
scopeId: panelContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { EuiTreeView } from '@elastic/eui';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { ExpandablePanel } from '../../shared/components/expandable_panel';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
|
||||
import { ANALYZER_PREVIEW_TITLE } from './translations';
|
||||
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { ANALYZE_GRAPH_ID } from '../../left/components/analyze_graph';
|
||||
import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
|
||||
import { getTreeNodes } from '../utils/analyzer_helpers';
|
||||
|
||||
export interface AnalyzerTreeProps {
|
||||
/**
|
||||
* statsNode data from resolver tree api
|
||||
*/
|
||||
statsNodes?: StatsNode[];
|
||||
/**
|
||||
* Boolean value of whether data is in loading
|
||||
*/
|
||||
loading: boolean;
|
||||
/**
|
||||
* Boolean value of whether there is error in data fetching
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Optional parameter to limit the number of child nodes to be displayed
|
||||
*/
|
||||
childCountLimit?: number;
|
||||
/**
|
||||
* Optional parameter to limit the depth of ancestors
|
||||
*/
|
||||
ancestorLevel?: number;
|
||||
/**
|
||||
* Optional parameter to limit the depth of descendants
|
||||
*/
|
||||
descendantLevel?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzer tree that represent a summary view of analyzer. It shows current process, and its parent and child processes
|
||||
*/
|
||||
export const AnalyzerTree: React.FC<AnalyzerTreeProps> = ({
|
||||
statsNodes,
|
||||
loading,
|
||||
error,
|
||||
childCountLimit = 3,
|
||||
ancestorLevel = 1,
|
||||
descendantLevel = 1,
|
||||
}) => {
|
||||
const { eventId, indexName, scopeId } = useRightPanelContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
const items = useMemo(
|
||||
() => getTreeNodes(statsNodes ?? [], childCountLimit, ancestorLevel, descendantLevel),
|
||||
[statsNodes, childCountLimit, ancestorLevel, descendantLevel]
|
||||
);
|
||||
|
||||
const goToAnalyserTab = useCallback(() => {
|
||||
openLeftPanel({
|
||||
id: LeftPanelKey,
|
||||
path: {
|
||||
tab: LeftPanelVisualizeTab,
|
||||
subTab: ANALYZE_GRAPH_ID,
|
||||
},
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
|
||||
if (items && items.length !== 0) {
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
title: ANALYZER_PREVIEW_TITLE,
|
||||
callback: goToAnalyserTab,
|
||||
iconType: 'arrowStart',
|
||||
}}
|
||||
content={{ loading, error }}
|
||||
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
|
||||
>
|
||||
<EuiTreeView
|
||||
items={items}
|
||||
display="compressed"
|
||||
aria-label={ANALYZER_PREVIEW_TITLE}
|
||||
showExpansionArrows
|
||||
/>
|
||||
</ExpandablePanel>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
AnalyzerTree.displayName = 'AnalyzerTree';
|
|
@ -68,6 +68,7 @@ const RELATED_ALERTS_BY_SESSION_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
|
|||
const RELATED_CASES_TEST_ID = SUMMARY_ROW_VALUE_TEST_ID(
|
||||
INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID
|
||||
);
|
||||
const CORRELATIONS_ERROR_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Error`;
|
||||
|
||||
const panelContextValue = {
|
||||
eventId: 'event id',
|
||||
|
@ -139,7 +140,7 @@ describe('<CorrelationsOverview />', () => {
|
|||
expect(getByTestId(RELATED_CASES_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide rows if show values are false', () => {
|
||||
it('should hide rows and show error message if show values are false', () => {
|
||||
jest
|
||||
.mocked(useShowRelatedAlertsByAncestry)
|
||||
.mockReturnValue({ show: false, documentId: 'documentId', indices: ['index1'] });
|
||||
|
@ -151,11 +152,12 @@ describe('<CorrelationsOverview />', () => {
|
|||
.mockReturnValue({ show: false, entityId: 'entityId' });
|
||||
jest.mocked(useShowRelatedCases).mockReturnValue(false);
|
||||
|
||||
const { queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
|
||||
const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
|
||||
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(CORRELATIONS_ERROR_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide rows if values are null', () => {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { RelatedCases } from './related_cases';
|
|||
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
|
||||
import { INSIGHTS_CORRELATIONS_TEST_ID } from './test_ids';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { CORRELATIONS_TITLE } from './translations';
|
||||
import { CORRELATIONS_ERROR, CORRELATIONS_TITLE } from './translations';
|
||||
import { LeftPanelKey, LeftPanelInsightsTab } from '../../left';
|
||||
import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details';
|
||||
|
||||
|
@ -69,6 +69,9 @@ export const CorrelationsOverview: React.FC = () => {
|
|||
const { show: showAlertsBySession, entityId } = useShowRelatedAlertsBySession({ getFieldsData });
|
||||
const showCases = useShowRelatedCases();
|
||||
|
||||
const canShowAtLeastOneInsight =
|
||||
showAlertsByAncestry || showSameSourceAlerts || showAlertsBySession || showCases;
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
|
@ -78,18 +81,22 @@ export const CorrelationsOverview: React.FC = () => {
|
|||
}}
|
||||
data-test-subj={INSIGHTS_CORRELATIONS_TEST_ID}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{showAlertsByAncestry && documentId && indices && (
|
||||
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
|
||||
)}
|
||||
{showSameSourceAlerts && originalEventId && (
|
||||
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
|
||||
)}
|
||||
{showAlertsBySession && entityId && (
|
||||
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
|
||||
)}
|
||||
{showCases && <RelatedCases eventId={eventId} />}
|
||||
</EuiFlexGroup>
|
||||
{canShowAtLeastOneInsight ? (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{showAlertsByAncestry && documentId && indices && (
|
||||
<RelatedAlertsByAncestry documentId={documentId} indices={indices} scopeId={scopeId} />
|
||||
)}
|
||||
{showSameSourceAlerts && originalEventId && (
|
||||
<RelatedAlertsBySameSourceEvent originalEventId={originalEventId} scopeId={scopeId} />
|
||||
)}
|
||||
{showAlertsBySession && entityId && (
|
||||
<RelatedAlertsBySession entityId={entityId} scopeId={scopeId} />
|
||||
)}
|
||||
{showCases && <RelatedCases eventId={eventId} />}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<div data-test-subj={`${INSIGHTS_CORRELATIONS_TEST_ID}Error`}>{CORRELATIONS_ERROR}</div>
|
||||
)}
|
||||
</ExpandablePanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,25 +12,9 @@ import { TestProviders } from '../../../common/mock';
|
|||
import React from 'react';
|
||||
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
|
||||
import { RightPanelContext } from '../context';
|
||||
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
|
||||
import { SESSION_VIEW_ID } from '../../left/components/session_view';
|
||||
import {
|
||||
EXPANDABLE_PANEL_CONTENT_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
|
||||
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
|
||||
} from '../../shared/components/test_ids';
|
||||
|
||||
jest.mock('../hooks/use_process_data');
|
||||
|
||||
const TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID);
|
||||
const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID);
|
||||
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID);
|
||||
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID);
|
||||
const CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID);
|
||||
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
|
@ -58,26 +42,6 @@ describe('SessionPreview', () => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should render wrapper component', () => {
|
||||
jest.mocked(useProcessData).mockReturnValue({
|
||||
processName: 'process1',
|
||||
userName: 'user1',
|
||||
startAt: '2022-01-01T00:00:00.000Z',
|
||||
ruleName: 'rule1',
|
||||
ruleId: 'id',
|
||||
workdir: '/path/to/workdir',
|
||||
command: 'command1',
|
||||
});
|
||||
|
||||
renderSessionPreview();
|
||||
|
||||
expect(screen.queryByTestId(TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(TITLE_LINK_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(TITLE_ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders session preview with all data', () => {
|
||||
jest.mocked(useProcessData).mockReturnValue({
|
||||
processName: 'process1',
|
||||
|
@ -95,7 +59,7 @@ describe('SessionPreview', () => {
|
|||
expect(screen.getByText('started')).toBeInTheDocument();
|
||||
expect(screen.getByText('process1')).toBeInTheDocument();
|
||||
expect(screen.getByText('at')).toBeInTheDocument();
|
||||
expect(screen.getByText('2022-01-01T00:00:00Z')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jan 1, 2022 @ 00:00:00.000')).toBeInTheDocument();
|
||||
expect(screen.getByText('with rule')).toBeInTheDocument();
|
||||
expect(screen.getByText('rule1')).toBeInTheDocument();
|
||||
expect(screen.getByText('by')).toBeInTheDocument();
|
||||
|
@ -122,29 +86,4 @@ describe('SessionPreview', () => {
|
|||
expect(screen.queryByText('with rule')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('by')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to left section Visualize tab when clicking on title', () => {
|
||||
jest.mocked(useProcessData).mockReturnValue({
|
||||
processName: 'process1',
|
||||
userName: 'user1',
|
||||
startAt: '2022-01-01T00:00:00.000Z',
|
||||
ruleName: 'rule1',
|
||||
ruleId: 'id',
|
||||
workdir: '/path/to/workdir',
|
||||
command: 'command1',
|
||||
});
|
||||
|
||||
const { getByTestId } = renderSessionPreview();
|
||||
|
||||
getByTestId(TITLE_LINK_TEST_ID).click();
|
||||
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
|
||||
id: LeftPanelKey,
|
||||
path: { tab: LeftPanelVisualizeTab, subTab: SESSION_VIEW_ID },
|
||||
params: {
|
||||
id: panelContextValue.eventId,
|
||||
indexName: panelContextValue.indexName,
|
||||
scopeId: panelContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,24 +6,19 @@
|
|||
*/
|
||||
|
||||
import { EuiCode, EuiIcon, useEuiTheme } from '@elastic/eui';
|
||||
import React, { useMemo, type FC, useCallback } from 'react';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { ExpandablePanel } from '../../shared/components/expandable_panel';
|
||||
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import React, { useMemo, type FC } from 'react';
|
||||
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
|
||||
import { useProcessData } from '../hooks/use_process_data';
|
||||
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
|
||||
import {
|
||||
SESSION_PREVIEW_COMMAND_TEXT,
|
||||
SESSION_PREVIEW_PROCESS_TEXT,
|
||||
SESSION_PREVIEW_RULE_TEXT,
|
||||
SESSION_PREVIEW_TIME_TEXT,
|
||||
SESSION_PREVIEW_TITLE,
|
||||
} from './translations';
|
||||
import { LeftPanelKey, LeftPanelVisualizeTab } from '../../left';
|
||||
import { RenderRuleName } from '../../../timelines/components/timeline/body/renderers/formatted_field_helpers';
|
||||
import { SESSION_VIEW_ID } from '../../left/components/session_view';
|
||||
|
||||
/**
|
||||
* One-off helper to make sure that inline values are rendered consistently
|
||||
|
@ -42,26 +37,10 @@ const ValueContainer: FC<{ text?: string }> = ({ text, children }) => (
|
|||
);
|
||||
|
||||
/**
|
||||
* Renders session preview under visualistions section in the flyout right EuiPanel
|
||||
* Renders session preview under Visualizations section in the flyout right EuiPanel
|
||||
*/
|
||||
export const SessionPreview: FC = () => {
|
||||
const { eventId, indexName, scopeId } = useRightPanelContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
|
||||
const goToSessionViewTab = useCallback(() => {
|
||||
openLeftPanel({
|
||||
id: LeftPanelKey,
|
||||
path: {
|
||||
tab: LeftPanelVisualizeTab,
|
||||
subTab: SESSION_VIEW_ID,
|
||||
},
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
}, [eventId, openLeftPanel, indexName, scopeId]);
|
||||
const { eventId, scopeId } = useRightPanelContext();
|
||||
|
||||
const { processName, userName, startAt, ruleName, ruleId, workdir, command } = useProcessData();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
@ -124,25 +103,16 @@ export const SessionPreview: FC = () => {
|
|||
}, [command, workdir]);
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
title: SESSION_PREVIEW_TITLE,
|
||||
iconType: 'arrowStart',
|
||||
callback: goToSessionViewTab,
|
||||
}}
|
||||
data-test-subj={SESSION_PREVIEW_TEST_ID}
|
||||
>
|
||||
<div>
|
||||
<ValueContainer>
|
||||
<EuiIcon type="user" />
|
||||
|
||||
<span style={emphasisStyles}>{userName}</span>
|
||||
</ValueContainer>
|
||||
{processNameFragment}
|
||||
{timeFragment}
|
||||
{ruleFragment}
|
||||
{commandFragment}
|
||||
</div>
|
||||
</ExpandablePanel>
|
||||
<div data-test-subj={SESSION_PREVIEW_TEST_ID}>
|
||||
<ValueContainer>
|
||||
<EuiIcon type="user" />
|
||||
|
||||
<span style={emphasisStyles}>{userName}</span>
|
||||
</ValueContainer>
|
||||
{processNameFragment}
|
||||
{timeFragment}
|
||||
{ruleFragment}
|
||||
{commandFragment}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import React from 'react';
|
||||
import { RightPanelContext } from '../context';
|
||||
import { SessionPreviewContainer } from './session_preview_container';
|
||||
import { useSessionPreview } from '../hooks/use_session_preview';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
|
||||
import {
|
||||
EXPANDABLE_PANEL_CONTENT_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
|
||||
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
|
||||
EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
|
||||
} from '../../shared/components/test_ids';
|
||||
import { mockGetFieldsData } from '../mocks/mock_context';
|
||||
|
||||
jest.mock('../hooks/use_session_preview');
|
||||
jest.mock('../../../common/hooks/use_license');
|
||||
|
||||
const panelContextValue = {
|
||||
dataAsNestedObject: null,
|
||||
getFieldsData: mockGetFieldsData,
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const sessionViewConfig = {
|
||||
index: {},
|
||||
sessionEntityId: 'sessionEntityId',
|
||||
sessionStartTime: 'sessionStartTime',
|
||||
};
|
||||
|
||||
const TEST_ID = SESSION_PREVIEW_TEST_ID;
|
||||
const ERROR_TEST_ID = `${SESSION_PREVIEW_TEST_ID}Error`;
|
||||
const UPSELL_TEST_ID = `${SESSION_PREVIEW_TEST_ID}UpSell`;
|
||||
|
||||
const renderSessionPreview = () =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<SessionPreviewContainer />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('SessionPreviewContainer', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should render component and link in header', () => {
|
||||
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
|
||||
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
|
||||
|
||||
const { getByTestId, queryByTestId } = renderSessionPreview();
|
||||
|
||||
expect(getByTestId(TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(ERROR_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(UPSELL_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message and text in header if no sessionConfig', () => {
|
||||
(useSessionPreview as jest.Mock).mockReturnValue(null);
|
||||
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
|
||||
|
||||
const { getByTestId, queryByTestId } = renderSessionPreview();
|
||||
|
||||
expect(queryByTestId(TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(ERROR_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(UPSELL_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message and text in header if no correct license', () => {
|
||||
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
|
||||
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => false });
|
||||
|
||||
const { getByTestId, queryByTestId } = renderSessionPreview();
|
||||
|
||||
expect(queryByTestId(TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(ERROR_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(UPSELL_TEST_ID)).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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, { type FC, useCallback } from 'react';
|
||||
import { TimelineTabs } from '@kbn/securitysolution-data-table';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLicense } from '../../../common/hooks/use_license';
|
||||
import { SessionPreview } from './session_preview';
|
||||
import { useSessionPreview } from '../hooks/use_session_preview';
|
||||
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions';
|
||||
import { ExpandablePanel } from '../../shared/components/expandable_panel';
|
||||
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
|
||||
import {
|
||||
SESSION_PREVIEW_ERROR,
|
||||
SESSION_PREVIEW_TITLE,
|
||||
SESSION_PREVIEW_UPSELL,
|
||||
} from './translations';
|
||||
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
|
||||
import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions';
|
||||
import { getScopedActions } from '../../../helpers';
|
||||
|
||||
const timelineId = 'timeline-1';
|
||||
|
||||
/**
|
||||
* Checks if the SessionView component is available, if so render it or else render an error message
|
||||
*/
|
||||
export const SessionPreviewContainer: FC = () => {
|
||||
const { dataAsNestedObject, getFieldsData } = useRightPanelContext();
|
||||
|
||||
// decide whether to show the session view or not
|
||||
const sessionViewConfig = useSessionPreview({ getFieldsData });
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
const isEnabled = sessionViewConfig && isEnterprisePlus;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
const scopedActions = getScopedActions(timelineId);
|
||||
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({
|
||||
ecsRowData: dataAsNestedObject,
|
||||
});
|
||||
|
||||
const goToSessionViewTab = useCallback(() => {
|
||||
// open timeline
|
||||
investigateInTimelineAlertClick();
|
||||
|
||||
// open session view tab
|
||||
startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW });
|
||||
if (sessionViewConfig !== null) {
|
||||
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session }));
|
||||
if (scopedActions) {
|
||||
dispatch(scopedActions.updateSessionViewConfig({ id: timelineId, sessionViewConfig }));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
investigateInTimelineAlertClick,
|
||||
scopedActions,
|
||||
sessionViewConfig,
|
||||
startTransaction,
|
||||
]);
|
||||
|
||||
const noSessionMessage = !isEnterprisePlus ? (
|
||||
<div data-test-subj={`${SESSION_PREVIEW_TEST_ID}UpSell`}>{SESSION_PREVIEW_UPSELL}</div>
|
||||
) : !sessionViewConfig ? (
|
||||
<div data-test-subj={`${SESSION_PREVIEW_TEST_ID}Error`}>{SESSION_PREVIEW_ERROR}</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<ExpandablePanel
|
||||
header={{
|
||||
title: SESSION_PREVIEW_TITLE,
|
||||
iconType: 'timeline',
|
||||
...(isEnabled && { callback: goToSessionViewTab }),
|
||||
}}
|
||||
data-test-subj={SESSION_PREVIEW_TEST_ID}
|
||||
>
|
||||
{isEnabled ? <SessionPreview /> : noSessionMessage}
|
||||
</ExpandablePanel>
|
||||
);
|
||||
};
|
|
@ -80,8 +80,6 @@ export const SUMMARY_ROW_VALUE_TEST_ID = (dataTestSubj: string) => `${dataTestSu
|
|||
/* Insights Entities */
|
||||
|
||||
export const INSIGHTS_ENTITIES_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsEntities';
|
||||
export const TECHNICAL_PREVIEW_ICON_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutTechnicalPreviewIcon';
|
||||
export const ENTITIES_USER_OVERVIEW_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutEntitiesUserOverview';
|
||||
export const ENTITIES_USER_OVERVIEW_LINK_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}Link`;
|
||||
|
@ -126,13 +124,7 @@ export const INSIGHTS_PREVALENCE_ROW_TEST_ID =
|
|||
export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle';
|
||||
export const VISUALIZATIONS_SECTION_HEADER_TEST_ID =
|
||||
'securitySolutionDocumentDetailsVisualizationsTitleHeader';
|
||||
|
||||
/* Visualizations analyzer preview */
|
||||
|
||||
export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerPreview';
|
||||
|
||||
/* Visualizations session preview */
|
||||
|
||||
export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalyzerPreview';
|
||||
export const SESSION_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsSessionPreview';
|
||||
|
||||
/* Response section */
|
||||
|
|
|
@ -145,6 +145,13 @@ export const CORRELATIONS_TITLE = i18n.translate(
|
|||
{ defaultMessage: 'Correlations' }
|
||||
);
|
||||
|
||||
export const CORRELATIONS_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.correlations.error',
|
||||
{
|
||||
defaultMessage: 'No correlations data available',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVALENCE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.prevalenceTitle',
|
||||
{ defaultMessage: 'Prevalence' }
|
||||
|
@ -195,6 +202,13 @@ export const ANALYZER_PREVIEW_TITLE = i18n.translate(
|
|||
{ defaultMessage: 'Analyzer preview' }
|
||||
);
|
||||
|
||||
export const ANALYZER_PREVIEW_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.analyzerPreview.error',
|
||||
{
|
||||
defaultMessage: 'No analyzer graph data available',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHARE = i18n.translate('xpack.securitySolution.flyout.documentDetails.share', {
|
||||
defaultMessage: 'Share Alert',
|
||||
});
|
||||
|
@ -213,6 +227,21 @@ export const SESSION_PREVIEW_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SESSION_PREVIEW_UPSELL = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.sessionPreview.upsell',
|
||||
{
|
||||
defaultMessage:
|
||||
'Session preview is disabled because your license does not support it. Please upgrade your license.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SESSION_PREVIEW_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.sessionPreview.error',
|
||||
{
|
||||
defaultMessage: 'No session view data available',
|
||||
}
|
||||
);
|
||||
|
||||
export const SESSION_PREVIEW_PROCESS_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.sessionPreview.processText',
|
||||
{
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { AnalyzerPreviewContainer } from './analyzer_preview_container';
|
||||
import { SessionPreviewContainer } from './session_preview_container';
|
||||
import { ExpandableSection } from './expandable_section';
|
||||
import { VISUALIZATIONS_SECTION_TEST_ID } from './test_ids';
|
||||
import { VISUALIZATIONS_TITLE } from './translations';
|
||||
import { AnalyzerPreview } from './analyzer_preview';
|
||||
import { SessionPreview } from './session_preview';
|
||||
|
||||
export interface VisualizatioinsSectionProps {
|
||||
export interface VisualizationsSectionProps {
|
||||
/**
|
||||
* Boolean to allow the component to be expanded or collapsed on first render
|
||||
*/
|
||||
|
@ -23,7 +23,7 @@ export interface VisualizatioinsSectionProps {
|
|||
/**
|
||||
* Visualizations section in overview. It contains analyzer preview and session view preview.
|
||||
*/
|
||||
export const VisualizationsSection: React.FC<VisualizatioinsSectionProps> = ({
|
||||
export const VisualizationsSection: React.FC<VisualizationsSectionProps> = ({
|
||||
expanded = false,
|
||||
}) => {
|
||||
return (
|
||||
|
@ -32,11 +32,11 @@ export const VisualizationsSection: React.FC<VisualizatioinsSectionProps> = ({
|
|||
title={VISUALIZATIONS_TITLE}
|
||||
data-test-subj={VISUALIZATIONS_SECTION_TEST_ID}
|
||||
>
|
||||
<SessionPreview />
|
||||
<SessionPreviewContainer />
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<AnalyzerPreview />
|
||||
<AnalyzerPreviewContainer />
|
||||
</ExpandableSection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 type { RenderHookResult } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import type { UseSessionPreviewParams } from './use_session_preview';
|
||||
import { useSessionPreview } from './use_session_preview';
|
||||
import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
|
||||
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
|
||||
|
||||
describe('useSessionPreview', () => {
|
||||
let hookResult: RenderHookResult<UseSessionPreviewParams, SessionViewConfig | null>;
|
||||
|
||||
it(`should return a session view config object`, () => {
|
||||
const getFieldsData: GetFieldsData = (field: string) => field;
|
||||
|
||||
hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
|
||||
initialProps: { getFieldsData },
|
||||
});
|
||||
|
||||
expect(hookResult.result.current).toEqual({
|
||||
index: 'kibana.alert.ancestors.index',
|
||||
investigatedAlertId: '_id',
|
||||
jumpToCursor: 'kibana.alert.original_time',
|
||||
jumpToEntityId: 'process.entity_id',
|
||||
sessionEntityId: 'process.entry_leader.entity_id',
|
||||
sessionStartTime: 'process.entry_leader.start',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return null if data isn't ready for session view`, () => {
|
||||
const getFieldsData: GetFieldsData = (field: string) => '';
|
||||
|
||||
hookResult = renderHook((props: UseSessionPreviewParams) => useSessionPreview(props), {
|
||||
initialProps: { getFieldsData },
|
||||
});
|
||||
|
||||
expect(hookResult.result.current).toEqual(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types';
|
||||
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
|
||||
import { getField } from '../../shared/utils';
|
||||
|
||||
export interface UseSessionPreviewParams {
|
||||
/**
|
||||
* Retrieves searchHit values for the provided field
|
||||
*/
|
||||
getFieldsData: GetFieldsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that returns the session view configuration if the session view is available for the alert
|
||||
*/
|
||||
export const useSessionPreview = ({
|
||||
getFieldsData,
|
||||
}: UseSessionPreviewParams): SessionViewConfig | null => {
|
||||
const _id = getField(getFieldsData('_id'));
|
||||
const index =
|
||||
getField(getFieldsData('kibana.alert.ancestors.index')) || getField(getFieldsData('_index'));
|
||||
const entryLeaderEntityId = getField(getFieldsData('process.entry_leader.entity_id'));
|
||||
const entryLeaderStart = getField(getFieldsData('process.entry_leader.start'));
|
||||
const entityId = getField(getFieldsData('process.entity_id'));
|
||||
const time =
|
||||
getField(getFieldsData('kibana.alert.original_time')) || getField(getFieldsData('timestamp'));
|
||||
|
||||
if (!index || !entryLeaderEntityId || !entryLeaderStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
sessionEntityId: entryLeaderEntityId,
|
||||
sessionStartTime: entryLeaderStart,
|
||||
...(entityId && { jumpToEntityId: entityId }),
|
||||
...(time && { jumpToCursor: time }),
|
||||
...(_id && { investigatedAlertId: _id }),
|
||||
};
|
||||
};
|
|
@ -26,7 +26,8 @@ import { getNewRule } from '../../../../objects/rule';
|
|||
import { ALERTS_URL } from '../../../../urls/navigation';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
|
||||
describe(
|
||||
// TODO enable once the visualize tabs are back
|
||||
describe.skip(
|
||||
'Alert details expandable flyout left panel analyzer graph',
|
||||
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
|
||||
() => {
|
||||
|
|
|
@ -42,7 +42,7 @@ describe(
|
|||
openEntitiesTab();
|
||||
});
|
||||
|
||||
it('should display analyzer graph and node list under Insights Entities', () => {
|
||||
it('should display host details and user details under Insights Entities', () => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Insights');
|
||||
|
|
|
@ -23,7 +23,8 @@ import { getNewRule } from '../../../../objects/rule';
|
|||
import { ALERTS_URL } from '../../../../urls/navigation';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
|
||||
describe(
|
||||
// TODO enable once the visualize tabs are back
|
||||
describe.skip(
|
||||
'Alert details expandable flyout left panel session view',
|
||||
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
|
||||
() => {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import {
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_CONTENT,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
|
||||
|
@ -42,7 +42,7 @@ import {
|
|||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE,
|
||||
|
@ -142,13 +142,15 @@ describe(
|
|||
|
||||
cy.log('analyzer graph preview');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).should('be.visible');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER).should(
|
||||
'be.visible'
|
||||
);
|
||||
|
||||
cy.log('session view preview');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).should('be.visible');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -32,15 +32,15 @@ import {
|
|||
INSIGHTS_THREAT_INTELLIGENCE_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_TEST_ID,
|
||||
INSIGHTS_PREVALENCE_TEST_ID,
|
||||
ANALYZER_PREVIEW_TEST_ID,
|
||||
SUMMARY_ROW_VALUE_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_RELATED_CASES_TEST_ID,
|
||||
INSIGHTS_ENTITIES_TEST_ID,
|
||||
SESSION_PREVIEW_TEST_ID,
|
||||
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
|
||||
ANALYZER_PREVIEW_TEST_ID,
|
||||
SESSION_PREVIEW_TEST_ID,
|
||||
} from '@kbn/security-solution-plugin/public/flyout/right/components/test_ids';
|
||||
import { getDataTestSubjectSelector } from '../../helpers/common';
|
||||
|
||||
|
@ -154,9 +154,9 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES =
|
|||
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER =
|
||||
getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT =
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTAINER =
|
||||
getDataTestSubjectSelector(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID));
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT =
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTAINER =
|
||||
getDataTestSubjectSelector(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID));
|
||||
|
||||
/* Response section */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue