Skip to content

Commit

Permalink
[Security Solution] Expandably flyout - Enable expandable flyout for …
Browse files Browse the repository at this point in the history
…generic events (elastic#176332)

## Summary

This PR enables the overview tab and left section insights for a generic
event. When user to go `host` or `user` page and expand details for an
event, in addition to table and json tab, they now have access to:

- Overview tab on the right section, which provide description of the
event kind or event category (detail logic linked in comment), key
insights such as highlighted fields, entities, prevalence and
visualization previews (if available)
- Expanded details that includes entities details and prevalence details

Many sections are shared by the alert details flyout, which we are
hoping to provide a unified experience when user opens the details
flyout.

#### When overview and expanded sections are enabled ####
- Ideally `event.kind` and `event.category` should be ecs compliant,
meaning the field values are of `allowed_values` within [ecs
definition](https://www.elastic.co/guide/en/ecs/current/ecs-event.html).
- If the field is not ecs compliant, and it does not fit the criteria to
generate an event renderer, the overview tab and expanded sections are
hidden
 
#### Variations depending on event kind ####
There is a variation of the about section depending on `event.kind`:
- `event.kind == 'event'`
- This is the most general and common event document, hence we provide
details at the `event.category` level.
- The title is also dynamic based on the category type (i.e if
`event.category` is process, the `process.name` is displayed)
- `event.kind != 'event'`
- These are events that not as common/general as `event` so we are
providing description at the `event.kind` level
   - The title matches the `event.kind` field
- `event.category` is included as a list of categories present for the
document
<img width="1006" alt="image"
src="https://github.com/elastic/kibana/assets/18648970/bb540c62-4346-4dc6-8c11-3ad6cdd1e7c9">

#### How to test ####
- Enable feature flag `expandableEventFlyoutEnabled`
- Generate some event data (the resolver generate data script is
sufficient to the test main logic, to get the event renderer to show up,
see comment on feeding additional data), alternatively, auditbeat and
filebeat also feed event data.
- Go to Explore -> Host -> Events table -> expand event details

### Checklist

Delete any items that are not applicable to this PR.

- [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
  • Loading branch information
christineweng authored Feb 22, 2024
1 parent 6d75e87 commit 250c427
Show file tree
Hide file tree
Showing 45 changed files with 1,501 additions and 375 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export const allowedExperimentalValues = Object.freeze({
* Enables expandable flyout in create rule page, alert preview
*/
expandableFlyoutInCreateRuleEnabled: true,
/**
* Enables expandable flyout for event type documents
*/
expandableEventFlyoutEnabled: false,
/*
* Enables new Set of filters on the Alerts page.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,28 @@ import type { VFC } from 'react';
import React, { useMemo } from 'react';
import { css } from '@emotion/react';
import type { LeftPanelPaths } from '.';
import { tabs } from './tabs';
import type { LeftPanelTabType } from './tabs';
import { FlyoutBody } from '../../shared/components/flyout_body';

export interface PanelContentProps {
/**
* Id of the tab selected in the parent component to display its content
*/
selectedTabId: LeftPanelPaths;
/**
* Tabs display at the top of left panel
*/
tabs: LeftPanelTabType[];
}

/**
* Document details expandable flyout left section. Appears after the user clicks on the expand details button in the right section.
* Displays the content of investigation and insights tabs (visualize is hidden for 8.9).
*/
export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId }) => {
export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId, tabs }) => {
const selectedTabContent = useMemo(() => {
return tabs.filter((tab) => tab.visible).find((tab) => tab.id === selectedTabId)?.content;
}, [selectedTabId]);
return tabs.find((tab) => tab.id === selectedTabId)?.content;
}, [selectedTabId, tabs]);

return (
<FlyoutBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import type { VFC } from 'react';
import React, { memo } from 'react';
import { css } from '@emotion/react';
import type { LeftPanelPaths } from '.';
import { tabs } from './tabs';
import { FlyoutHeader } from '../../shared/components/flyout_header';
import type { LeftPanelTabType } from './tabs';
import { getField } from '../shared/utils';
import { EventKind } from '../shared/constants/event_kinds';
import { useLeftPanelContext } from './context';

export interface PanelHeaderProps {
/**
Expand All @@ -23,17 +26,23 @@ export interface PanelHeaderProps {
* @param selected
*/
setSelectedTabId: (selected: LeftPanelPaths) => void;
/**
* Tabs display at the top of left panel
*/
tabs: LeftPanelTabType[];
}

/**
* Header at the top of the left section.
* Displays the investigation and insights tabs (visualize is hidden for 8.9).
* Displays the insights, investigation and response 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) => (
export const PanelHeader: VFC<PanelHeaderProps> = memo(
({ selectedTabId, setSelectedTabId, tabs }) => {
const { getFieldsData } = useLeftPanelContext();
const isEventKindSignal = getField(getFieldsData('event.kind')) === EventKind.signal;

const onSelectedTabChanged = (id: LeftPanelPaths) => setSelectedTabId(id);
const renderTabs = tabs.map((tab, index) => (
<EuiTab
onClick={() => onSelectedTabChanged(tab.id)}
isSelected={tab.id === selectedTabId}
Expand All @@ -44,19 +53,20 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo(({ selectedTabId, setSele
</EuiTab>
));

return (
<FlyoutHeader
css={css`
background-color: ${useEuiBackgroundColor('subdued')};
padding-bottom: 0 !important;
border-block-end: none !important;
`}
>
<EuiTabs size="l" expand>
{renderTabs}
</EuiTabs>
</FlyoutHeader>
);
});
return (
<FlyoutHeader
css={css`
background-color: ${useEuiBackgroundColor('subdued')};
padding-bottom: 0 !important;
border-block-end: none !important;
`}
>
<EuiTabs size="l" expand={isEventKindSignal}>
{renderTabs}
</EuiTabs>
</FlyoutHeader>
);
}
);

PanelHeader.displayName = 'PanelHeader';
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { PanelHeader } from './header';
import { PanelContent } from './content';
import type { LeftPanelTabsType } from './tabs';
import { tabs } from './tabs';
import type { LeftPanelTabType } from './tabs';
import * as tabs from './tabs';
import { getField } from '../shared/utils';
import { EventKind } from '../shared/constants/event_kinds';
import { useLeftPanelContext } from './context';

export type LeftPanelPaths = 'visualize' | 'insights' | 'investigation' | 'response';
Expand All @@ -34,16 +36,24 @@ export interface LeftPanelProps extends FlyoutPanelProps {

export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {
const { openLeftPanel } = useExpandableFlyoutApi();
const { eventId, indexName, scopeId } = useLeftPanelContext();
const { eventId, indexName, scopeId, getFieldsData } = useLeftPanelContext();
const eventKind = getField(getFieldsData('event.kind'));

const tabsDisplayed = useMemo(
() =>
eventKind === EventKind.signal
? [tabs.insightsTab, tabs.investigationTab, tabs.responseTab]
: [tabs.insightsTab],
[eventKind]
);

const selectedTabId = useMemo(() => {
const visibleTabs = tabs.filter((tab) => tab.visible);
const defaultTab = visibleTabs[0].id;
const defaultTab = tabsDisplayed[0].id;
if (!path) return defaultTab;
return visibleTabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
}, [path]);
return tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab;
}, [path, tabsDisplayed]);

const setSelectedTabId = (tabId: LeftPanelTabsType[number]['id']) => {
const setSelectedTabId = (tabId: LeftPanelTabType['id']) => {
openLeftPanel({
id: DocumentDetailsLeftPanelKey,
path: {
Expand All @@ -59,8 +69,12 @@ export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {

return (
<>
<PanelHeader selectedTabId={selectedTabId} setSelectedTabId={setSelectedTabId} />
<PanelContent selectedTabId={selectedTabId} />
<PanelHeader
selectedTabId={selectedTabId}
setSelectedTabId={setSelectedTabId}
tabs={tabsDisplayed}
/>
<PanelContent selectedTabId={selectedTabId} tabs={tabsDisplayed} />
</>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,61 +20,57 @@ import {
} from './test_ids';
import { ResponseTab } from './tabs/response_tab';

export type LeftPanelTabsType = Array<{
export interface LeftPanelTabType {
id: LeftPanelPaths;
'data-test-subj': string;
name: ReactElement;
content: React.ReactElement;
visible: boolean;
}>;
}

export const tabs: LeftPanelTabsType = [
{
id: 'visualize',
'data-test-subj': VISUALIZE_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.visualize.tabLabel"
defaultMessage="Visualize"
/>
),
content: <VisualizeTab />,
visible: false,
},
{
id: 'insights',
'data-test-subj': INSIGHTS_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.tabLabel"
defaultMessage="Insights"
/>
),
content: <InsightsTab />,
visible: true,
},
{
id: 'investigation',
'data-test-subj': INVESTIGATION_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigations.tabLabel"
defaultMessage="Investigation"
/>
),
content: <InvestigationTab />,
visible: true,
},
{
id: 'response',
'data-test-subj': RESPONSE_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.tabLabel"
defaultMessage="Response"
/>
),
content: <ResponseTab />,
visible: true,
},
];
export const visualizeTab: LeftPanelTabType = {
id: 'visualize',
'data-test-subj': VISUALIZE_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.visualize.tabLabel"
defaultMessage="Visualize"
/>
),
content: <VisualizeTab />,
};

export const insightsTab: LeftPanelTabType = {
id: 'insights',
'data-test-subj': INSIGHTS_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.tabLabel"
defaultMessage="Insights"
/>
),
content: <InsightsTab />,
};

export const investigationTab: LeftPanelTabType = {
id: 'investigation',
'data-test-subj': INVESTIGATION_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigations.tabLabel"
defaultMessage="Investigation"
/>
),
content: <InvestigationTab />,
};

export const responseTab: LeftPanelTabType = {
id: 'response',
'data-test-subj': RESPONSE_TAB_TEST_ID,
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.tabLabel"
defaultMessage="Response"
/>
),
content: <ResponseTab />,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
* 2.0.
*/

import React, { memo, useCallback } from 'react';

import React, { memo, useCallback, useMemo } from 'react';
import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group';
import { i18n } from '@kbn/i18n';
Expand All @@ -28,6 +27,8 @@ import {
} from '../components/threat_intelligence_details';
import { PREVALENCE_TAB_ID, PrevalenceDetails } from '../components/prevalence_details';
import { CORRELATIONS_TAB_ID, CorrelationsDetails } from '../components/correlations_details';
import { getField } from '../../shared/utils';
import { EventKind } from '../../shared/constants/event_kinds';

const insightsButtons: EuiButtonGroupOptionProps[] = [
{
Expand Down Expand Up @@ -76,11 +77,25 @@ const insightsButtons: EuiButtonGroupOptionProps[] = [
* Insights view displayed in the document details expandable flyout left section
*/
export const InsightsTab: React.FC = memo(() => {
const { eventId, indexName, scopeId } = useLeftPanelContext();
const { eventId, indexName, scopeId, getFieldsData } = useLeftPanelContext();
const isEventKindSignal = getField(getFieldsData('event.kind')) === EventKind.signal;
const { openLeftPanel } = useExpandableFlyoutApi();
const panels = useExpandableFlyoutState();
const activeInsightsId = panels.left?.path?.subTab ?? ENTITIES_TAB_ID;

// insight tabs based on whether document is alert or non-alert
// alert: entities, threat intelligence, prevalence, correlations
// non-alert: entities, prevalence
const buttonGroup = useMemo(
() =>
isEventKindSignal
? insightsButtons
: insightsButtons.filter(
(tab) => tab.id === ENTITIES_TAB_ID || tab.id === PREVALENCE_TAB_ID
),
[isEventKindSignal]
);

const onChangeCompressed = useCallback(
(optionId: string) => {
openLeftPanel({
Expand All @@ -103,19 +118,17 @@ export const InsightsTab: React.FC = memo(() => {
<>
<EuiButtonGroup
color="primary"
name="coarsness"
legend={i18n.translate(
'xpack.securitySolution.flyout.left.insights.buttonGroupLegendLabel',
{
defaultMessage: 'Insights options',
}
{ defaultMessage: 'Insights options' }
)}
options={insightsButtons}
options={buttonGroup}
idSelected={activeInsightsId}
onChange={onChangeCompressed}
buttonSize="compressed"
isFullWidth
data-test-subj={INSIGHTS_TAB_BUTTON_GROUP_TEST_ID}
style={!isEventKindSignal ? { maxWidth: 300 } : undefined}
/>
<EuiSpacer size="m" />
{activeInsightsId === ENTITIES_TAB_ID && <EntitiesDetails />}
Expand Down
Loading

0 comments on commit 250c427

Please sign in to comment.