[Security Solution] expandable flyout - show full alert reason in preview panel (#163667)

This commit is contained in:
Philippe Oberti 2023-08-16 14:39:34 +02:00 committed by GitHub
parent e78d61789f
commit 736b16dfcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 437 additions and 262 deletions

View file

@ -10,6 +10,7 @@ import { type Criteria, EuiBasicTable, formatDate, EuiEmptyPrompt } from '@elast
import { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { isRight } from 'fp-ts/lib/Either';
import { ALERT_REASON, ALERT_RULE_NAME } from '@kbn/rule-data-utils';
import { SeverityBadge } from '../../../detections/components/rules/severity_badge';
import { usePaginatedAlerts } from '../hooks/use_paginated_alerts';
import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations';
@ -26,12 +27,12 @@ export const columns = [
render: (value: string) => formatDate(value, TIMESTAMP_DATE_FORMAT),
},
{
field: 'kibana.alert.rule.name',
field: ALERT_RULE_NAME,
name: i18n.CORRELATIONS_RULE_COLUMN_TITLE,
truncateText: true,
},
{
field: 'kibana.alert.reason',
field: ALERT_REASON,
name: i18n.CORRELATIONS_REASON_COLUMN_TITLE,
truncateText: true,
},

View file

@ -0,0 +1,47 @@
/*
* 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 { PreviewPanelContext } from '../context';
import { mockContextValue } from '../mocks/mock_preview_panel_context';
import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids';
import { AlertReasonPreview } from './alert_reason_preview';
import { ThemeProvider } from 'styled-components';
import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
const mockTheme = getMockTheme({ eui: { euiFontSizeXS: '' } });
const panelContextValue = {
...mockContextValue,
};
describe('<AlertReasonPreview />', () => {
it('should render alert reason preview', () => {
const { getByTestId } = render(
<PreviewPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<AlertReasonPreview />
</ThemeProvider>
</PreviewPanelContext.Provider>
);
expect(getByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).toBeInTheDocument();
});
it('should render null is dataAsNestedObject is null', () => {
const contextValue = {
...mockContextValue,
dataAsNestedObject: null,
};
const { queryByTestId } = render(
<PreviewPanelContext.Provider value={contextValue}>
<AlertReasonPreview />
</PreviewPanelContext.Provider>
);
expect(queryByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,50 @@
/*
* 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, { useMemo } from 'react';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { ALERT_REASON_TITLE } from './translations';
import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids';
import { usePreviewPanelContext } from '../context';
import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
/**
* Alert reason renderer on a preview panel on top of the right section of expandable flyout
*/
export const AlertReasonPreview: React.FC = () => {
const { dataAsNestedObject } = usePreviewPanelContext();
const renderer = useMemo(
() =>
dataAsNestedObject != null
? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers })
: null,
[dataAsNestedObject]
);
if (!dataAsNestedObject || !renderer) {
return null;
}
return (
<EuiPanel hasShadow={false} data-test-subj={ALERT_REASON_PREVIEW_BODY_TEST_ID}>
<EuiTitle>
<h6>{ALERT_REASON_TITLE}</h6>
</EuiTitle>
<EuiSpacer size="m" />
{renderer.renderRow({
contextId: 'event-details',
data: dataAsNestedObject,
isDraggable: false,
scopeId: 'global',
})}
</EuiPanel>
);
};
AlertReasonPreview.displayName = 'AlertReasonPreview';

View file

@ -38,3 +38,5 @@ export const RULE_PREVIEW_LOADING_TEST_ID =
'securitySolutionDocumentDetailsFlyoutRulePreviewLoadingSpinner';
export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter';
export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails';
export const ALERT_REASON_PREVIEW_BODY_TEST_ID =
'securitySolutionDocumentDetailsFlyoutAlertReasonPreviewBody';

View file

@ -31,3 +31,8 @@ export const RULE_PREVIEW_ACTIONS_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewActionsSectionText',
{ defaultMessage: 'Actions' }
);
export const ALERT_REASON_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.alertReasonTitle',
{ defaultMessage: 'Alert reason' }
);

View file

@ -7,11 +7,15 @@
import React, { createContext, useContext, useMemo } from 'react';
import type { DataViewBase } from '@kbn/es-query';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import type { PreviewPanelProps } from '.';
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 { 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';
export interface PreviewPanelContext {
/**
@ -34,6 +38,10 @@ export interface PreviewPanelContext {
* Index pattern for rule details
*/
indexPattern: DataViewBase;
/**
* An object with top level fields from the ECS object
*/
dataAsNestedObject: Ecs | null;
}
export const PreviewPanelContext = createContext<PreviewPanelContext | undefined>(undefined);
@ -52,12 +60,21 @@ export const PreviewPanelProvider = ({
ruleId,
children,
}: PreviewPanelProviderProps) => {
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 [_, __, ___, dataAsNestedObject] = useTimelineEventsDetails({
indexName: eventIndex,
eventId: id ?? '',
runtimeMappings: sourcererDataView.runtimeMappings,
skip: !id,
});
const contextValue = useMemo(
() =>
id && indexName && scopeId
@ -67,9 +84,10 @@ export const PreviewPanelProvider = ({
scopeId,
ruleId: ruleId ?? '',
indexPattern: sourcererDataView.indexPattern,
dataAsNestedObject,
}
: undefined,
[id, indexName, scopeId, ruleId, sourcererDataView.indexPattern]
[id, indexName, scopeId, ruleId, sourcererDataView.indexPattern, dataAsNestedObject]
);
return (

View file

@ -10,8 +10,9 @@ import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { panels } from './panels';
export type PreviewPanelPaths = 'rule-preview';
export type PreviewPanelPaths = 'rule-preview' | 'alert-reason-preview';
export const RulePreviewPanel: PreviewPanelPaths = 'rule-preview';
export const AlertReasonPreviewPanel: PreviewPanelPaths = 'alert-reason-preview';
export const PreviewPanelKey: PreviewPanelProps['key'] = 'document-details-preview';
export interface PreviewPanelProps extends FlyoutPanelProps {

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { mockDataAsNestedObject } from '../../shared/mocks/mock_context';
import type { PreviewPanelContext } from '../context';
/**
@ -16,4 +18,5 @@ export const mockContextValue: PreviewPanelContext = {
scopeId: 'scopeId',
ruleId: '',
indexPattern: { fields: [], title: 'test index' },
dataAsNestedObject: mockDataAsNestedObject as unknown as Ecs,
};

View file

@ -6,8 +6,9 @@
*/
import React from 'react';
import { AlertReasonPreview } from './components/alert_reason_preview';
import type { PreviewPanelPaths } from '.';
import { RULE_PREVIEW } from './translations';
import { ALERT_REASON_PREVIEW, RULE_PREVIEW } from './translations';
import { RulePreview } from './components/rule_preview';
import { RulePreviewFooter } from './components/rule_preview_footer';
@ -40,4 +41,9 @@ export const panels: PreviewPanelType = [
content: <RulePreview />,
footer: <RulePreviewFooter />,
},
{
id: 'alert-reason-preview',
name: ALERT_REASON_PREVIEW,
content: <AlertReasonPreview />,
},
];

View file

@ -11,3 +11,8 @@ export const RULE_PREVIEW = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewPanel',
{ defaultMessage: 'Rule preview' }
);
export const ALERT_REASON_PREVIEW = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.alertReasonPreviewPanel',
{ defaultMessage: 'Alert reason preview' }
);

View file

@ -1,92 +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 { css } from '@emotion/react';
import type { Story } from '@storybook/react';
import { Description } from './description';
import { RightPanelContext } from '../context';
const ruleUuid = {
category: 'kibana',
field: 'kibana.alert.rule.uuid',
values: ['123'],
originalValue: ['123'],
isObjectArray: false,
};
const ruleDescription = {
category: 'kibana',
field: 'kibana.alert.rule.description',
values: [
`This is a very long description of the rule. In theory. this description is long enough that it should be cut off when displayed in collapsed mode. If it isn't then there is a problem`,
],
originalValue: ['description'],
isObjectArray: false,
};
export default {
component: Description,
title: 'Flyout/Description',
};
const wrapper = (children: React.ReactNode, panelContextValue: RightPanelContext) => (
<RightPanelContext.Provider value={panelContextValue}>
<div
css={css`
width: 500px;
`}
>
{children}
</div>
</RightPanelContext.Provider>
);
export const Rule: Story<void> = () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [ruleUuid, ruleDescription],
} as unknown as RightPanelContext;
return wrapper(<Description />, panelContextValue);
};
export const Document: Story<void> = () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [
{
category: 'kibana',
field: 'kibana.alert.rule.description',
values: ['This is a description for the document.'],
originalValue: ['description'],
isObjectArray: false,
},
],
} as unknown as RightPanelContext;
return wrapper(<Description />, panelContextValue);
};
export const EmptyDescription: Story<void> = () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [
ruleUuid,
{
category: 'kibana',
field: 'kibana.alert.rule.description',
values: [''],
originalValue: ['description'],
isObjectArray: false,
},
],
} as unknown as RightPanelContext;
return wrapper(<Description />, panelContextValue);
};
export const Empty: Story<void> = () => {
const panelContextValue = {} as unknown as RightPanelContext;
return wrapper(<Description />, panelContextValue);
};

View file

@ -8,14 +8,17 @@
import React from 'react';
import { render } from '@testing-library/react';
import { DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID } from './test_ids';
import { DOCUMENT_DESCRIPTION_TITLE, RULE_DESCRIPTION_TITLE } from './translations';
import {
DOCUMENT_DESCRIPTION_TITLE,
PREVIEW_RULE_DETAILS,
RULE_DESCRIPTION_TITLE,
} from './translations';
import { Description } from './description';
import { TestProviders } from '../../../common/mock';
import { RightPanelContext } from '../context';
import { ThemeProvider } from 'styled-components';
import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
import { mockGetFieldsData } from '../mocks/mock_context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { PreviewPanelKey } from '../../preview';
const ruleUuid = {
category: 'kibana',
@ -41,23 +44,32 @@ const ruleName = {
isObjectArray: false,
};
jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to');
const flyoutContextValue = {
openPreviewPanel: jest.fn(),
} as unknown as ExpandableFlyoutContext;
const panelContextValue = (dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null) =>
({
eventId: 'event id',
indexName: 'indexName',
scopeId: 'scopeId',
dataFormattedForFieldBrowser,
getFieldsData: mockGetFieldsData,
} as unknown as RightPanelContext);
const renderDescription = (panelContext: RightPanelContext) =>
render(
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={panelContext}>
<Description />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
describe('<Description />', () => {
it('should render the component', () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [ruleUuid, ruleDescription, ruleName],
} as unknown as RightPanelContext;
const { getByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
const { getByTestId } = renderDescription(
panelContextValue([ruleUuid, ruleDescription, ruleName])
);
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
@ -66,18 +78,8 @@ describe('<Description />', () => {
});
it('should not render rule preview button if rule name is not available', () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [ruleUuid, ruleDescription],
} as unknown as RightPanelContext;
const { getByTestId, queryByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
const { getByTestId, queryByTestId } = renderDescription(
panelContextValue([ruleUuid, ruleDescription])
);
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
@ -86,21 +88,44 @@ describe('<Description />', () => {
});
it('should render document title if document is not an alert', () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [ruleDescription],
} as unknown as RightPanelContext;
const { getByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<ThemeProvider theme={mockTheme}>
<Description />
</ThemeProvider>
</RightPanelContext.Provider>
</TestProviders>
);
const { getByTestId } = renderDescription(panelContextValue([ruleDescription]));
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent(DOCUMENT_DESCRIPTION_TITLE);
});
it('should render null if dataFormattedForFieldBrowser is null', () => {
const panelContext = {
...panelContextValue([ruleUuid, ruleDescription, ruleName]),
dataFormattedForFieldBrowser: null,
} as unknown as RightPanelContext;
const { container } = renderDescription(panelContext);
expect(container).toBeEmptyDOMElement();
});
it('should open preview panel when clicking on button', () => {
const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]);
const { getByTestId } = renderDescription(panelContext);
getByTestId(RULE_SUMMARY_BUTTON_TEST_ID).click();
expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({
id: PreviewPanelKey,
path: { tab: 'rule-preview' },
params: {
id: panelContext.eventId,
indexName: panelContext.indexName,
scopeId: panelContext.scopeId,
banner: {
title: PREVIEW_RULE_DETAILS,
backgroundColor: 'warning',
textColor: 'warning',
},
ruleId: ruleUuid.values[0],
},
});
});
});

View file

@ -1,45 +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 { StorybookProviders } from '../../../common/mock/storybook_providers';
import { Reason } from './reason';
import { RightPanelContext } from '../context';
import { mockDataAsNestedObject, mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
export default {
component: Reason,
title: 'Flyout/Reason',
};
export const Default: Story<void> = () => {
const panelContextValue = {
dataAsNestedObject: mockDataAsNestedObject,
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
} as unknown as RightPanelContext;
return (
<StorybookProviders>
<RightPanelContext.Provider value={panelContextValue}>
<Reason />
</RightPanelContext.Provider>
</StorybookProviders>
);
};
export const Empty: Story<void> = () => {
const panelContextValue = {
dataFormattedForFieldBrowser: {},
} as unknown as RightPanelContext;
return (
<RightPanelContext.Provider value={panelContextValue}>
<Reason />
</RightPanelContext.Provider>
);
};

View file

@ -7,70 +7,85 @@
import React from 'react';
import { render } from '@testing-library/react';
import { REASON_TITLE_TEST_ID } from './test_ids';
import {
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
REASON_DETAILS_TEST_ID,
REASON_TITLE_TEST_ID,
} from './test_ids';
import { Reason } from './reason';
import { RightPanelContext } from '../context';
import { mockDataAsNestedObject, mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
import { euiDarkVars } from '@kbn/ui-theme';
import { ThemeProvider } from 'styled-components';
import { mockDataFormattedForFieldBrowser, mockGetFieldsData } from '../mocks/mock_context';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { PreviewPanelKey } from '../../preview';
import { PREVIEW_ALERT_REASON_DETAILS } from './translations';
const flyoutContextValue = {
openPreviewPanel: jest.fn(),
} as unknown as ExpandableFlyoutContext;
const panelContextValue = {
eventId: 'event id',
indexName: 'indexName',
scopeId: 'scopeId',
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
getFieldsData: mockGetFieldsData,
} as unknown as RightPanelContext;
const renderReason = (panelContext: RightPanelContext = panelContextValue) =>
render(
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={panelContext}>
<Reason />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
describe('<Reason />', () => {
it('should render the component', () => {
const panelContextValue = {
dataAsNestedObject: mockDataAsNestedObject,
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
} as unknown as RightPanelContext;
const { getByTestId } = render(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<RightPanelContext.Provider value={panelContextValue}>
<Reason />
</RightPanelContext.Provider>
</ThemeProvider>
);
const { getByTestId } = renderReason();
expect(getByTestId(REASON_TITLE_TEST_ID)).toBeInTheDocument();
});
it('should render null if dataFormattedForFieldBrowser is null', () => {
const panelContextValue = {
dataAsNestedObject: {},
const panelContext = {
...panelContextValue,
dataFormattedForFieldBrowser: null,
} as unknown as RightPanelContext;
const { container } = render(
<RightPanelContext.Provider value={panelContextValue}>
<Reason />
</RightPanelContext.Provider>
);
const { container } = renderReason(panelContext);
expect(container).toBeEmptyDOMElement();
});
it('should render null if dataAsNestedObject is null', () => {
const panelContextValue = {
dataFormattedForFieldBrowser: [],
it('should render no reason if the field is null', () => {
const panelContext = {
...panelContextValue,
getFieldsData: () => {},
} as unknown as RightPanelContext;
const { container } = render(
<RightPanelContext.Provider value={panelContextValue}>
<Reason />
</RightPanelContext.Provider>
);
const { getByTestId } = renderReason(panelContext);
expect(container).toBeEmptyDOMElement();
expect(getByTestId(REASON_DETAILS_TEST_ID)).toBeEmptyDOMElement();
});
it('should render null if renderer is null', () => {
const panelContextValue = {
dataAsNestedObject: {},
dataFormattedForFieldBrowser: [],
} as unknown as RightPanelContext;
const { container } = render(
<RightPanelContext.Provider value={panelContextValue}>
<Reason />
</RightPanelContext.Provider>
);
it('should open preview panel when clicking on button', () => {
const { getByTestId } = renderReason();
expect(container).toBeEmptyDOMElement();
getByTestId(REASON_DETAILS_PREVIEW_BUTTON_TEST_ID).click();
expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({
id: PreviewPanelKey,
path: { tab: 'alert-reason-preview' },
params: {
id: panelContextValue.eventId,
indexName: panelContextValue.indexName,
scopeId: panelContextValue.scopeId,
banner: {
title: PREVIEW_ALERT_REASON_DETAILS,
backgroundColor: 'warning',
textColor: 'warning',
},
},
});
});
});

View file

@ -6,31 +6,71 @@
*/
import type { FC } from 'react';
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID } from './test_ids';
import { ALERT_REASON_TITLE, DOCUMENT_REASON_TITLE } from './translations';
import React, { useCallback, useMemo } from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { ALERT_REASON } from '@kbn/rule-data-utils';
import { getField } from '../../shared/utils';
import { AlertReasonPreviewPanel, PreviewPanelKey } from '../../preview';
import {
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
REASON_DETAILS_TEST_ID,
REASON_TITLE_TEST_ID,
} from './test_ids';
import {
ALERT_REASON_DETAILS_TEXT,
ALERT_REASON_TITLE,
DOCUMENT_REASON_TITLE,
PREVIEW_ALERT_REASON_DETAILS,
} from './translations';
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer';
import { useRightPanelContext } from '../context';
/**
* Displays the information provided by the rowRenderer. Supports multiple types of documents.
*/
export const Reason: FC = () => {
const { dataAsNestedObject, dataFormattedForFieldBrowser } = useRightPanelContext();
const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, getFieldsData } =
useRightPanelContext();
const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const alertReason = getField(getFieldsData(ALERT_REASON));
const renderer = useMemo(
() =>
dataAsNestedObject != null
? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers })
: null,
[dataAsNestedObject]
const { openPreviewPanel } = useExpandableFlyoutContext();
const openRulePreview = useCallback(() => {
openPreviewPanel({
id: PreviewPanelKey,
path: { tab: AlertReasonPreviewPanel },
params: {
id: eventId,
indexName,
scopeId,
banner: {
title: PREVIEW_ALERT_REASON_DETAILS,
backgroundColor: 'warning',
textColor: 'warning',
},
},
});
}, [eventId, openPreviewPanel, indexName, scopeId]);
const viewPreview = useMemo(
() => (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
iconType="expand"
onClick={openRulePreview}
iconSide="right"
data-test-subj={REASON_DETAILS_PREVIEW_BUTTON_TEST_ID}
>
{ALERT_REASON_DETAILS_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
),
[openRulePreview]
);
if (!dataFormattedForFieldBrowser || !dataAsNestedObject || !renderer) {
if (!dataFormattedForFieldBrowser) {
return null;
}
@ -38,17 +78,21 @@ export const Reason: FC = () => {
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem data-test-subj={REASON_TITLE_TEST_ID}>
<EuiTitle size="xxs">
<h5>{isAlert ? ALERT_REASON_TITLE : DOCUMENT_REASON_TITLE}</h5>
<h5>
{isAlert ? (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<h5>{ALERT_REASON_TITLE}</h5>
</EuiFlexItem>
{viewPreview}
</EuiFlexGroup>
) : (
DOCUMENT_REASON_TITLE
)}
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem data-test-subj={REASON_DETAILS_TEST_ID}>
{renderer.renderRow({
contextId: 'event-details',
data: dataAsNestedObject,
isDraggable: false,
scopeId: 'global',
})}
</EuiFlexItem>
<EuiFlexItem data-test-subj={REASON_DETAILS_TEST_ID}>{alertReason}</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -46,6 +46,8 @@ export const DESCRIPTION_DETAILS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutDescriptionDetails';
export const REASON_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonTitle';
export const REASON_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonDetails';
export const REASON_DETAILS_PREVIEW_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutReasonDetailsPreviewButton';
export const MITRE_ATTACK_TITLE_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackTitle';
export const MITRE_ATTACK_DETAILS_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackDetails';

View file

@ -45,6 +45,13 @@ export const RULE_SUMMARY_TEXT = i18n.translate(
}
);
export const ALERT_REASON_DETAILS_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.alertReasonDetailsText',
{
defaultMessage: 'Show full reason',
}
);
/* About section */
export const ABOUT_TITLE = i18n.translate(
@ -66,6 +73,11 @@ export const PREVIEW_RULE_DETAILS = i18n.translate(
{ defaultMessage: 'Preview rule details' }
);
export const PREVIEW_ALERT_REASON_DETAILS = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.previewAlertReasonDetailsText',
{ defaultMessage: 'Preview alert reason' }
);
export const DOCUMENT_DESCRIPTION_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.documentDescriptionTitle',
{

View file

@ -6,6 +6,7 @@
*/
import { useMemo } from 'react';
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data';
import { getField } from '../../shared/utils';
import { useRightPanelContext } from '../context';
@ -14,8 +15,6 @@ const FIELD_USER_NAME = 'process.entry_leader.user.name' as const;
const FIELD_USER_ID = 'process.entry_leader.user.id' as const;
const FIELD_PROCESS_NAME = 'process.entry_leader.name' as const;
const FIELD_START_AT = 'process.entry_leader.start' as const;
const FIELD_RULE_NAME = 'kibana.alert.rule.name' as const;
const FIELD_RULE_ID = 'kibana.alert.rule.uuid' as const;
const FIELD_WORKING_DIRECTORY = 'process.group_leader.working_directory' as const;
const FIELD_COMMAND = 'process.command_line' as const;
@ -48,8 +47,8 @@ export const useProcessData = () => {
userName: getUserDisplayName(getFieldsData),
processName: getField(getFieldsData(FIELD_PROCESS_NAME)),
startAt: getField(getFieldsData(FIELD_START_AT)),
ruleName: getField(getFieldsData(FIELD_RULE_NAME)),
ruleId: getField(getFieldsData(FIELD_RULE_ID)),
ruleName: getField(getFieldsData(ALERT_RULE_NAME)),
ruleId: getField(getFieldsData(ALERT_RULE_UUID)),
workdir: getField(getFieldsData(FIELD_WORKING_DIRECTORY)),
command: getField(getFieldsData(FIELD_COMMAND)),
}),

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils';
import { ALERT_REASON, ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils';
/**
* Returns mocked data for field (mock this method: x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts)
@ -22,6 +22,8 @@ export const mockGetFieldsData = (field: string): string[] => {
return ['host1'];
case 'user.name':
return ['user1'];
case ALERT_REASON:
return ['reason'];
default:
return [];
}

View file

@ -0,0 +1,42 @@
/*
* 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_ALERT_REASON_PREVIEW_CONTAINER } from '../../../../screens/expandable_flyout/alert_details_preview_panel_alert_reason_preview';
import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common';
import { clickAlertReasonButton } from '../../../../tasks/expandable_flyout/alert_details_right_panel_overview_tab';
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';
import { tag } from '../../../../tags';
describe(
'Alert details expandable flyout rule preview panel',
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
() => {
const rule = getNewRule();
beforeEach(() => {
cleanKibana();
login();
createRule(rule);
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlertExpandableFlyout();
clickAlertReasonButton();
});
describe('alert reason preview', () => {
it('should display alert reason preview', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER).should('be.visible');
});
});
}
);

View file

@ -111,7 +111,8 @@ describe(
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE)
.should('be.visible')
.and('have.text', 'Alert reason');
.and('contain.text', 'Alert reason')
.and('contain.text', 'Show full reason');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS)
.should('be.visible')
.and('contain.text', rule.name);

View file

@ -0,0 +1,13 @@
/*
* 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 { ALERT_REASON_PREVIEW_BODY_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/preview/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
export const DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER = getDataTestSubjectSelector(
ALERT_REASON_PREVIEW_BODY_TEST_ID
);

View file

@ -38,6 +38,7 @@ import {
ANALYZER_PREVIEW_CONTENT_TEST_ID,
SESSION_PREVIEW_CONTENT_TEST_ID,
INSIGHTS_PREVALENCE_VALUE_TEST_ID,
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/right/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
@ -59,6 +60,8 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE =
getDataTestSubjectSelector(REASON_TITLE_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS =
getDataTestSubjectSelector(REASON_DETAILS_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON =
getDataTestSubjectSelector(REASON_DETAILS_PREVIEW_BUTTON_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE = getDataTestSubjectSelector(
MITRE_ATTACK_TITLE_TEST_ID
);

View file

@ -20,6 +20,8 @@ import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON,
} from '../../screens/expandable_flyout/alert_details_right_panel_overview_tab';
/* About section */
@ -129,3 +131,17 @@ export const clickRuleSummaryButton = () => {
.click();
});
};
/**
* Click `Show full reason` button to open alert reason preview panel
*/
export const clickAlertReasonButton = () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE).scrollIntoView();
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE)
.should('be.visible')
.within(() => {
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON)
.should('be.visible')
.click();
});
};