Skip to content

Commit

Permalink
[Security Solution][Document Details] Add an advanced setting for vis…
Browse files Browse the repository at this point in the history
…ualizations in flyout in ESS (#194012)

## Summary

This PR replaces a feature flag `visualizationInFlyoutEnabled` with
advanced setting `securitySolution:enableVisualizationsInFlyout`.


![image](https://github.com/user-attachments/assets/3ddf00d8-d641-44ae-aca6-45a4c1bcbd7e)

#### When advanced setting is off (DEFAULT):
- Visualize tab should not be present in alert/event flyout 
- Analyzer and session preview links should go to timeline

#### When advanced setting is on:
- Visualize tab is present in alert/event flyout 
- The analyzer and session preview icon should open left panel


![image](https://github.com/user-attachments/assets/ca25538f-2f7c-4fc7-9081-473c6b9b3a5b)

#### Some enhancements and fixes:
- Clicking alerts in session viewer opens an alert preview
- Upsell and no data messages are updated in session viewer to be
consistent with session preview
- Links in analyzer and session preview should be disabled in previews
(`isPreviewMode`)
- Links in analyzer and session preview should be disabled in rule
preview (`isPreview`)



https://github.com/user-attachments/assets/074166b8-3ce1-4488-9245-029b7dc55c59


### 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
  • Loading branch information
christineweng authored Oct 1, 2024
1 parent 121ff39 commit 9d671f6
Show file tree
Hide file tree
Showing 26 changed files with 953 additions and 215 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-management/settings/setting_ids/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ export const SECURITY_SOLUTION_EXCLUDE_COLD_AND_FROZEN_TIERS_IN_ANALYZER =
/** This Kibana Advanced Setting allows users to enable/disable the Asset Criticality feature */
export const SECURITY_SOLUTION_ENABLE_ASSET_CRITICALITY_SETTING =
'securitySolution:enableAssetCriticality' as const;
export const SECURITY_SOLUTION_ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING =
'securitySolution:enableVisualizationsInFlyout' as const;

// Timelion settings
export const TIMELION_ES_DEFAULT_INDEX_ID = 'timelion:es.default_index';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'securitySolution:enableVisualizationsInFlyout': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'search:includeFrozen': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface UsageStats {
'securitySolution:enableAssetCriticality': boolean;
'securitySolution:excludeColdAndFrozenTiersInAnalyzer': boolean;
'securitySolution:enableCcsWarning': boolean;
'securitySolution:enableVisualizationsInFlyout': boolean;
'search:includeFrozen': boolean;
'courier:maxConcurrentShardRequests': number;
'courier:setRequestPreference': string;
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/telemetry/schema/oss_plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -9919,6 +9919,12 @@
"description": "Non-default value of setting."
}
},
"securitySolution:enableVisualizationsInFlyout":{
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"search:includeFrozen": {
"type": "boolean",
"_meta": {
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ export const ENABLE_ASSET_CRITICALITY_SETTING = 'securitySolution:enableAssetCri
export const EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION =
'securitySolution:excludedDataTiersForRuleExecution' as const;

/** This Kibana Advanced Setting allows users to enable/disable the Visualizations in Flyout feature */
export const ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING =
'securitySolution:enableVisualizationsInFlyout' as const;

/**
* Id for the notifications alerting type
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
analyzerDatePickersAndSourcererDisabled: false,

/**
* Enables visualization: session viewer and analyzer in expandable flyout
*/
visualizationInFlyoutEnabled: false,

/**
* Enables an ability to customize Elastic prebuilt rules.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,35 @@ import '@testing-library/jest-dom';
import { DocumentDetailsContext } from '../../shared/context';
import { TestProviders } from '../../../../common/mock';
import { SESSION_VIEW_TEST_ID } from './test_ids';
import {
SESSION_VIEW_UPSELL_TEST_ID,
SESSION_VIEW_NO_DATA_TEST_ID,
} from '../../shared/components/test_ids';
import { SessionView } from './session_view';
import {
ANCESTOR_INDEX,
ENTRY_LEADER_ENTITY_ID,
ENTRY_LEADER_START,
} from '../../shared/constants/field_names';
import { useSessionPreview } from '../../right/hooks/use_session_preview';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import { mockContextValue } from '../../shared/mocks/mock_context';
import { useLicense } from '../../../../common/hooks/use_license';

jest.mock('../../right/hooks/use_session_preview');
jest.mock('../../../../common/hooks/use_license');
jest.mock('../../../../sourcerer/containers');

const NO_DATA_MESSAGE =
'You can only view Linux session details if you’ve enabled the Include session data setting in your Elastic Defend integration policy. Refer to Enable Session View dataExternal link(opens in a new tab or window) for more information.';

const UPSELL_TEXT = 'This feature requires an Enterprise subscription';

const sessionViewConfig = {
index: {},
sessionEntityId: 'sessionEntityId',
sessionStartTime: 'sessionStartTime',
};

interface MockData {
[key: string]: string;
Expand Down Expand Up @@ -46,7 +69,7 @@ jest.mock('../../../../common/lib/kibana', () => {
};
});

const renderSessionView = (contextValue: DocumentDetailsContext) =>
const renderSessionView = (contextValue: DocumentDetailsContext = mockContextValue) =>
render(
<TestProviders>
<DocumentDetailsContext.Provider value={contextValue}>
Expand All @@ -56,6 +79,19 @@ const renderSessionView = (contextValue: DocumentDetailsContext) =>
);

describe('<SessionView />', () => {
beforeEach(() => {
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
jest.mocked(useSourcererDataView).mockReturnValue({
browserFields: {},
dataViewId: '',
loading: false,
indicesExist: true,
selectedPatterns: ['index'],
indexPattern: { fields: [], title: '' },
sourcererDataView: undefined,
});
});
it('renders session view correctly', () => {
const contextValue = {
getFieldsData: mockFieldsData,
Expand All @@ -75,4 +111,19 @@ describe('<SessionView />', () => {
const wrapper = renderSessionView(contextValue);
expect(wrapper.getByTestId(SESSION_VIEW_TEST_ID)).toBeInTheDocument();
});

it('should render upsell message in header if no correct license', () => {
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => false });

const { getByTestId } = renderSessionView();
expect(getByTestId(SESSION_VIEW_UPSELL_TEST_ID)).toHaveTextContent(UPSELL_TEXT);
});

it('should render error message and text in header if no sessionConfig', () => {
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
(useSessionPreview as jest.Mock).mockReturnValue(null);

const { getByTestId } = renderSessionView();
expect(getByTestId(SESSION_VIEW_NO_DATA_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,99 @@
*/

import type { FC } from 'react';
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import type { TableId } from '@kbn/securitysolution-data-table';
import { EuiPanel } from '@elastic/eui';
import {
ANCESTOR_INDEX,
ENTRY_LEADER_ENTITY_ID,
ENTRY_LEADER_START,
} from '../../shared/constants/field_names';
import { getField } from '../../shared/utils';
import { SESSION_VIEW_TEST_ID } from './test_ids';
import { isActiveTimeline } from '../../../../helpers';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import { DocumentDetailsPreviewPanelKey } from '../../shared/constants/panel_keys';
import { useKibana } from '../../../../common/lib/kibana';
import { useDocumentDetailsContext } from '../../shared/context';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
import { detectionsTimelineIds } from '../../../../timelines/containers/helpers';
import { ALERT_PREVIEW_BANNER } from '../../preview/constants';
import { useLicense } from '../../../../common/hooks/use_license';
import { useSessionPreview } from '../../right/hooks/use_session_preview';
import { SessionViewNoDataMessage } from '../../shared/components/session_view_no_data_message';

export const SESSION_VIEW_ID = 'session-view';

/**
* Session view displayed in the document details expandable flyout left section under the Visualize tab
*/
export const SessionView: FC = () => {
const { sessionView } = useKibana().services;
const { getFieldsData, indexName } = useDocumentDetailsContext();
const { sessionView, telemetry } = useKibana().services;
const { getFieldsData, indexName, scopeId, dataFormattedForFieldBrowser } =
useDocumentDetailsContext();

const sessionViewConfig = useSessionPreview({ getFieldsData, dataFormattedForFieldBrowser });
const isEnterprisePlus = useLicense().isEnterprise();
const isEnabled = sessionViewConfig && isEnterprisePlus;

const ancestorIndex = getField(getFieldsData(ANCESTOR_INDEX)); // e.g in case of alert, we want to grab it's origin index
const sessionEntityId = getField(getFieldsData(ENTRY_LEADER_ENTITY_ID)) || '';
const sessionStartTime = getField(getFieldsData(ENTRY_LEADER_START)) || '';
const index = ancestorIndex || indexName;

// TODO as part of https://github.com/elastic/security-team/issues/7031
// bring back no data message if needed
const sourcererScope = useMemo(() => {
if (isActiveTimeline(scopeId)) {
return SourcererScopeName.timeline;
} else if (detectionsTimelineIds.includes(scopeId as TableId)) {
return SourcererScopeName.detections;
} else {
return SourcererScopeName.default;
}
}, [scopeId]);

const { selectedPatterns } = useSourcererDataView(sourcererScope);
const eventDetailsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]);

const { openPreviewPanel } = useExpandableFlyoutApi();
const openAlertDetailsPreview = useCallback(
(eventId?: string, onClose?: () => void) => {
openPreviewPanel({
id: DocumentDetailsPreviewPanelKey,
params: {
id: eventId,
indexName: eventDetailsIndex,
scopeId,
banner: ALERT_PREVIEW_BANNER,
isPreviewMode: true,
},
});
telemetry.reportDetailsFlyoutOpened({
location: scopeId,
panel: 'preview',
});
},
[openPreviewPanel, eventDetailsIndex, scopeId, telemetry]
);

return (
return isEnabled ? (
<div data-test-subj={SESSION_VIEW_TEST_ID}>
{sessionView.getSessionView({
index,
sessionEntityId,
sessionStartTime,
isFullScreen: true,
loadAlertDetails: openAlertDetailsPreview,
})}
</div>
) : (
<EuiPanel hasShadow={false}>
<SessionViewNoDataMessage
isEnterprisePlus={isEnterprisePlus}
hasSessionViewConfig={sessionViewConfig !== null}
/>
</EuiPanel>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import type { FC } from 'react';
import React, { memo, useMemo } from 'react';

import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../common/constants';
import { DocumentDetailsLeftPanelKey } from '../shared/constants/panel_keys';
import { useKibana } from '../../../common/lib/kibana';
import { PanelHeader } from './header';
Expand Down Expand Up @@ -38,8 +40,8 @@ export const LeftPanel: FC<Partial<DocumentDetailsProps>> = memo(({ path }) => {
'securitySolutionNotesEnabled'
);

const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled(
'visualizationInFlyoutEnabled'
const [visualizationInFlyoutEnabled] = useUiSetting$<boolean>(
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING
);

const tabsDisplayed = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useDocumentDetailsContext } from '../../shared/context';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import {
DocumentDetailsLeftPanelKey,
DocumentDetailsAnalyzerPanelKey,
} from '../../shared/constants/panel_keys';
import { LeftPanelVisualizeTab } from '..';
import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys';
import {
VISUALIZE_TAB_BUTTON_GROUP_TEST_ID,
VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID,
Expand Down Expand Up @@ -59,8 +55,8 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [
* Visualize view displayed in the document details expandable flyout left section
*/
export const VisualizeTab = memo(() => {
const { eventId, indexName, scopeId } = useDocumentDetailsContext();
const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi();
const { scopeId } = useDocumentDetailsContext();
const { openPreviewPanel } = useExpandableFlyoutApi();
const panels = useExpandableFlyoutState();
const [activeVisualizationId, setActiveVisualizationId] = useState(
panels.left?.path?.subTab ?? SESSION_VIEW_ID
Expand All @@ -72,28 +68,16 @@ export const VisualizeTab = memo(() => {
setActiveVisualizationId(optionId);
if (optionId === ANALYZE_GRAPH_ID) {
startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER });
openPreviewPanel({
id: DocumentDetailsAnalyzerPanelKey,
params: {
resolverComponentInstanceID: `${key}-${scopeId}`,
banner: ANALYZER_PREVIEW_BANNER,
},
});
}
openLeftPanel({
id: DocumentDetailsLeftPanelKey,
path: {
tab: LeftPanelVisualizeTab,
subTab: optionId,
},
params: {
id: eventId,
indexName,
scopeId,
},
});
openPreviewPanel({
id: DocumentDetailsAnalyzerPanelKey,
params: {
resolverComponentInstanceID: `${key}-${scopeId}`,
banner: ANALYZER_PREVIEW_BANNER,
},
});
},
[startTransaction, eventId, indexName, scopeId, openLeftPanel, openPreviewPanel, key]
[startTransaction, openPreviewPanel, key, scopeId]
);

useEffect(() => {
Expand Down
Loading

0 comments on commit 9d671f6

Please sign in to comment.