Skip to content

Commit

Permalink
[Security Solution] Use new expandable flyout in alert preview (elast…
Browse files Browse the repository at this point in the history
…ic#167902)

## Summary

This PR updates the alert preview in Create rule -> Rule preview to use
the new expandable alert flyout:

- Switched timeline wrapper to be visible on create rule page. This
allows us to keep all the timeline navigation in the new expandable
alert flyout
- Disabled alert specific components, when flyout is open in create
rule:
   - Alert status is not shown
   - Rule summary preview is disabled
   - Title link to rule details page is removed
   - Exclude filter in/filter out hover actions in highlighted fields
- New placeholder text for investigation guide and response: we should
not show link to documentation when user is setting up a rule

With feature flag on:


https://github.com/elastic/kibana/assets/18648970/a45e930e-f1e8-4899-aef4-1aa0c3dc3330



**How to test**
- Add `xpack.securitySolution.enableExperimental:
['expandableFlyoutInCreateRuleEnabled' ]` to `kibana.yml.dev`
- Go to Rules page -> Detection rules (SIEM) => Create rule
- Pick a rule type and populate the query, click `Continue`
- On the right hand side, click `Refresh`, some alerts should appear in
the table
- Click expand on a row

### Checklist

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

---------

Co-authored-by: Nikita Indik <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
3 people authored Nov 27, 2023
1 parent 192519d commit f5648d9
Show file tree
Hide file tree
Showing 31 changed files with 325 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,14 @@ export const allowedExperimentalValues = Object.freeze({
* Enables top charts on Alerts Page
*/
alertsPageChartsEnabled: true,
/**
* Enables the alert type column in KPI visualizations on Alerts Page
*/
alertTypeEnabled: false,

/**
* Enables expandable flyout in create rule page, alert preview
*/
expandableFlyoutInCreateRuleEnabled: false,
/*
* Enables new Set of filters on the Alerts page.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
import { TimelineId } from '../../../../../common/types';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';

type Props = EuiDataGridCellValueElementProps & {
columnHeaders: ColumnHeaderOptions[];
Expand Down Expand Up @@ -73,6 +74,9 @@ const RowActionComponent = ({

const dispatch = useDispatch();
const [isSecurityFlyoutEnabled] = useUiSetting$<boolean>(ENABLE_EXPANDABLE_FLYOUT_SETTING);
const isExpandableFlyoutInCreateRuleEnabled = useIsExperimentalFeatureEnabled(
'expandableFlyoutInCreateRuleEnabled'
);

const columnValues = useMemo(
() =>
Expand All @@ -89,6 +93,13 @@ const RowActionComponent = ({
[columnHeaders, timelineNonEcsData]
);

let showExpandableFlyout: boolean;
if (tableId === TableId.rulePreview) {
showExpandableFlyout = isSecurityFlyoutEnabled && isExpandableFlyoutInCreateRuleEnabled;
} else {
showExpandableFlyout = isSecurityFlyoutEnabled;
}

const handleOnEventDetailPanelOpened = useCallback(() => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'eventDetail',
Expand All @@ -98,9 +109,7 @@ const RowActionComponent = ({
},
};

// TODO remove when https://github.com/elastic/security-team/issues/7760 is merged
// excluding rule preview page as some sections in new flyout are not applicable when user is creating a new rule
if (isSecurityFlyoutEnabled && tableId !== TableId.rulePreview) {
if (showExpandableFlyout) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
Expand Down Expand Up @@ -133,7 +142,7 @@ const RowActionComponent = ({
})
);
}
}, [dispatch, eventId, indexName, isSecurityFlyoutEnabled, openFlyout, tabType, tableId]);
}, [dispatch, eventId, indexName, openFlyout, tabType, tableId, showExpandableFlyout]);

const Action = controlColumn.rowCellRender;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('use show timeline', () => {
});

it('hides timeline for blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' });
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/add_rules' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jest.mock('../../shared/hooks/use_investigation_guide');

const NO_DATA_TEXT =
"There's no investigation guide for this rule. Edit the rule's settingsExternal link(opens in a new tab or window) to add one.";
const PREVIEW_MESSAGE = 'Investigation guide is not available in alert preview.';

const renderInvestigationGuide = (context: LeftPanelContext = mockContextValue) => (
<TestProviders>
Expand Down Expand Up @@ -76,4 +77,15 @@ describe('<InvestigationGuide />', () => {
const { getByTestId } = render(renderInvestigationGuide());
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_TEXT);
});

it('should render preview message when flyout is in preview', () => {
(useInvestigationGuide as jest.Mock).mockReturnValue({
loading: false,
error: true,
});
const { getByTestId } = render(
renderInvestigationGuide({ ...mockContextValue, isPreview: true })
);
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ import { FlyoutLoading } from '../../../shared/components/flyout_loading';
* Renders a message saying the guide hasn't been set up or the full investigation guide.
*/
export const InvestigationGuide: React.FC = () => {
const { dataFormattedForFieldBrowser } = useLeftPanelContext();
const { dataFormattedForFieldBrowser, isPreview } = useLeftPanelContext();

const { loading, error, basicAlertData, ruleNote } = useInvestigationGuide({
dataFormattedForFieldBrowser,
});

return (
<div data-test-subj={INVESTIGATION_GUIDE_TEST_ID}>
{loading ? (
{isPreview ? (
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigation.previewMessage"
defaultMessage="Investigation guide is not available in alert preview."
/>
) : loading ? (
<FlyoutLoading data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID} />
) : !error && basicAlertData.ruleId && ruleNote ? (
<InvestigationGuideView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jest.mock('../../../../common/lib/kibana', () => {

const NO_DATA_MESSAGE =
"There are no response actions defined for this event. To add some, edit the rule's settings and set up response actionsExternal link(opens in a new tab or window).";
const PREVIEW_MESSAGE = 'Response is not available in alert preview.';

const defaultContextValue = {
dataAsNestedObject: {
Expand Down Expand Up @@ -139,4 +140,10 @@ describe('<ResponseDetails />', () => {

expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});

it('should render preview message if flyout is in preview', () => {
const wrapper = renderResponseDetails({ ...defaultContextValue, isPreview: true });
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toBeInTheDocument();
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toHaveTextContent(PREVIEW_MESSAGE);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ExtendedFlyoutWrapper = styled.div`
* Automated response actions results, displayed in the document details expandable flyout left section under the Insights tab, Response tab
*/
export const ResponseDetails: React.FC = () => {
const { searchHit, dataAsNestedObject } = useLeftPanelContext();
const { searchHit, dataAsNestedObject, isPreview } = useLeftPanelContext();
const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled(
'endpointResponseActionsEnabled'
);
Expand All @@ -40,19 +40,28 @@ export const ResponseDetails: React.FC = () => {

return (
<div data-test-subj={RESPONSE_DETAILS_TEST_ID}>
<EuiTitle size="xxxs">
<h5>
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.responseTitle"
defaultMessage="Responses"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
{isPreview ? (
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.previewMessage"
defaultMessage="Response is not available in alert preview."
/>
) : (
<>
<EuiTitle size="xxxs">
<h5>
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.responseTitle"
defaultMessage="Responses"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />

<ExtendedFlyoutWrapper>
{endpointResponseActionsEnabled ? responseActionsView?.content : osqueryView?.content}
</ExtendedFlyoutWrapper>
<ExtendedFlyoutWrapper>
{endpointResponseActionsEnabled ? responseActionsView?.content : osqueryView?.content}
</ExtendedFlyoutWrapper>
</>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import React, { createContext, memo, useContext, useMemo } from 'react';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { TableId } from '@kbn/securitysolution-data-table';
import { useEventDetails } from '../shared/hooks/use_event_details';
import { FlyoutError } from '../../shared/components/flyout_error';
import { FlyoutLoading } from '../../shared/components/flyout_loading';
Expand Down Expand Up @@ -54,6 +55,10 @@ export interface LeftPanelContext {
* Retrieves searchHit values for the provided field
*/
getFieldsData: GetFieldsData;
/**
* Boolean to indicate whether it is a preview flyout
*/
isPreview: boolean;
}

export const LeftPanelContext = createContext<LeftPanelContext | undefined>(undefined);
Expand Down Expand Up @@ -97,6 +102,7 @@ export const LeftPanelProvider = memo(
searchHit,
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
getFieldsData,
isPreview: scopeId === TableId.rulePreview,
}
: undefined,
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ export const mockContextValue: LeftPanelContext = {
searchHit: mockSearchHit,
dataAsNestedObject: mockDataAsNestedObject,
investigationFields: [],
isPreview: false,
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ const panelContextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
};

const renderAnalyzerPreview = () =>
const renderAnalyzerPreview = (context = panelContextValue) =>
render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<RightPanelContext.Provider value={context}>
<AnalyzerPreviewContainer />
</RightPanelContext.Provider>
</TestProviders>
Expand Down Expand Up @@ -117,7 +117,7 @@ describe('AnalyzerPreviewContainer', () => {
).toHaveTextContent(NO_ANALYZER_MESSAGE);
});

it('should navigate to left section Visualize tab when clicking on title', () => {
it('should navigate to analyzer in timeline when clicking on title', () => {
(isInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
(useAlertPrevalenceFromProcessTree as jest.Mock).mockReturnValue({
loading: false,
Expand All @@ -136,4 +136,24 @@ describe('AnalyzerPreviewContainer', () => {
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID)).click();
expect(investigateInTimelineAlertClick).toHaveBeenCalled();
});

it('should not navigate to analyzer when in preview and 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 { queryByTestId } = renderAnalyzerPreview({ ...panelContextValue, isPreview: true });
expect(
queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({});
expect(investigateInTimelineAlertClick).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const timelineId = 'timeline-1';
* Analyzer preview under Overview, Visualizations. It shows a tree representation of analyzer.
*/
export const AnalyzerPreviewContainer: React.FC = () => {
const { dataAsNestedObject } = useRightPanelContext();
const { dataAsNestedObject, isPreview } = useRightPanelContext();

// decide whether to show the analyzer preview or not
const isEnabled = isInvestigateInResolverActionEnabled(dataAsNestedObject);
Expand Down Expand Up @@ -64,17 +64,18 @@ export const AnalyzerPreviewContainer: React.FC = () => {
/>
),
iconType: 'timeline',
...(isEnabled && {
link: {
callback: goToAnalyzerTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip"
defaultMessage="Show analyzer graph"
/>
),
},
}),
...(isEnabled &&
!isPreview && {
link: {
callback: goToAnalyzerTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewTooltip"
defaultMessage="Show analyzer graph"
/>
),
},
}),
}}
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ describe('<Description />', () => {
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toHaveAttribute('disabled');
});

it('should render rule preview button as disabled if flyout is in preview', () => {
const { getByTestId } = renderDescription({
...panelContextValue([{ ...ruleUuid, values: [] }, ruleName, ruleDescription]),
isPreview: true,
});
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toHaveAttribute('disabled');
});

it('should open preview panel when clicking on button', () => {
const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
* If the document is an alert we show the rule description. If the document is of another type, we show -.
*/
export const Description: FC = () => {
const { dataFormattedForFieldBrowser, scopeId, eventId, indexName } = useRightPanelContext();
const { dataFormattedForFieldBrowser, scopeId, eventId, indexName, isPreview } =
useRightPanelContext();
const { isAlert, ruleDescription, ruleName, ruleId } = useBasicDataFromDetailsData(
dataFormattedForFieldBrowser
);
Expand Down Expand Up @@ -75,7 +76,7 @@ export const Description: FC = () => {
defaultMessage: 'Show rule summary',
}
)}
disabled={isEmpty(ruleName) || isEmpty(ruleId)}
disabled={isEmpty(ruleName) || isEmpty(ruleId) || isPreview}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel"
Expand All @@ -84,7 +85,7 @@ export const Description: FC = () => {
</EuiButtonEmpty>
</EuiFlexItem>
),
[ruleName, openRulePreview, ruleId]
[ruleName, openRulePreview, ruleId, isPreview]
);

const alertRuleDescription =
Expand Down
Loading

0 comments on commit f5648d9

Please sign in to comment.