[Cloud Security] Converts graph visualization from experimental feature to the advanced settings (#208614)

## Summary

During 8.17 development cycle, we introduced a new feature behind an
experimental flag.
Now, in 8.18/9.0 a user could toggle the feature through the advanced
settings.
This will allow users on serverless to use this feature.
This change does not introduce a breaking change.

![Screenshot 2025-01-28 at 20 05
03](https://github.com/user-attachments/assets/80187838-ee8d-462a-aefd-04f9b96cd0c8)

### How to test 

1. Toggle the feature flags
2. Load mock data

```bash
node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \ 
  --es-url http://elastic:changeme@localhost:9200 \
  --kibana-url http://elastic:changeme@localhost:5601

node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \
  --es-url http://elastic:changeme@localhost:9200 \
  --kibana-url http://elastic:changeme@localhost:5601
```

3. Go to the alerts page
4. Change the query time range to show alerts from the 13th of October
2024 (**IMPORTANT**)
5. Open the alerts flyout
6. Scroll to see the graph visualization : D


### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kfir Peled 2025-01-29 10:48:51 +00:00 committed by GitHub
parent 413033aa8f
commit dbd06296cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 128 additions and 46 deletions

View file

@ -178,6 +178,8 @@ 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;
export const SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING =
'securitySolution:enableGraphVisualization' as const;
// Timelion settings
export const TIMELION_ES_DEFAULT_INDEX_ID = 'timelion:es.default_index';

View file

@ -24,4 +24,5 @@ export const SECURITY_PROJECT_SETTINGS = [
settings.SECURITY_SOLUTION_ENABLE_NEWS_FEED_ID,
settings.SECURITY_SOLUTION_DEFAULT_ALERT_TAGS_KEY,
settings.SECURITY_SOLUTION_ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING,
settings.SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING,
];

View file

@ -134,6 +134,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'securitySolution:enableGraphVisualization': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'search:includeFrozen': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },

View file

@ -73,6 +73,7 @@ export interface UsageStats {
'securitySolution:excludeColdAndFrozenTiersInAnalyzer': boolean;
'securitySolution:enableCcsWarning': boolean;
'securitySolution:enableVisualizationsInFlyout': boolean;
'securitySolution:enableGraphVisualization': boolean;
'search:includeFrozen': boolean;
'courier:maxConcurrentShardRequests': number;
'courier:setRequestPreference': string;

View file

@ -10493,6 +10493,12 @@
"description": "Non-default value of setting."
}
},
"securitySolution:enableGraphVisualization": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"search:includeFrozen": {
"type": "boolean",
"_meta": {

View file

@ -11,6 +11,7 @@ import {
} from '@kbn/cloud-security-posture-common/schema/graph/latest';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1';
import { SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING } from '@kbn/management-settings-ids';
import { GRAPH_ROUTE_PATH } from '../../../common/constants';
import { CspRequestHandlerContext, CspRouter } from '../../types';
import { getGraph as getGraphV1 } from './v1';
@ -45,6 +46,15 @@ export const defineGraphRoute = (router: CspRouter) =>
const { nodesLimit, showUnknownTarget = false } = request.body;
const { originEventIds, start, end, esQuery } = request.body.query as GraphRequest['query'];
const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id;
const isGraphEnabled = await (
await context.core
).uiSettings.client.get(SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING);
cspContext.logger.debug(`isGraphEnabled: ${isGraphEnabled}`);
if (!isGraphEnabled) {
return response.notFound();
}
try {
const resp = await getGraphV1({

View file

@ -65,6 +65,7 @@
"@kbn/cloud-security-posture-common",
"@kbn/cloud-security-posture",
"@kbn/analytics",
"@kbn/management-settings-ids",
],
"exclude": ["target/**/*"]
}

View file

@ -213,6 +213,10 @@ export const MAX_UNASSOCIATED_NOTES = 'securitySolution:maxUnassociatedNotes' as
export const ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING =
'securitySolution:enableVisualizationsInFlyout' as const;
/** This Kibana Advanced Setting allows users to enable/disable the Graph Visualizations for alerts and events */
export const ENABLE_GRAPH_VISUALIZATION_SETTING =
'securitySolution:enableGraphVisualization' 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

View file

@ -189,11 +189,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
jamfDataInAnalyzerEnabled: true,
/**
* Enables graph visualization in alerts flyout
*/
graphVisualizationInFlyoutEnabled: false,
/**
* Enables an ability to customize Elastic prebuilt rules.
*

View file

@ -11,7 +11,7 @@ import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/butt
import { useExpandableFlyoutApi, useExpandableFlyoutState } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import { useDocumentDetailsContext } from '../../shared/context';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys';
@ -31,7 +31,7 @@ import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
import { GRAPH_ID, GraphVisualization } from '../components/graph_visualization';
import { useGraphPreview } from '../../shared/hooks/use_graph_preview';
import { GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE } from '../../shared/constants/experimental_features';
import { ENABLE_GRAPH_VISUALIZATION_SETTING } from '../../../../../common/constants';
const visualizeButtons: EuiButtonGroupOptionProps[] = [
{
@ -127,13 +127,11 @@ export const VisualizeTab = memo(() => {
dataFormattedForFieldBrowser,
});
const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled(
GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE
);
const [graphVisualizationEnabled] = useUiSetting$<boolean>(ENABLE_GRAPH_VISUALIZATION_SETTING);
const options = [...visualizeButtons];
if (hasGraphRepresentation && isGraphFeatureEnabled) {
if (hasGraphRepresentation && graphVisualizationEnabled) {
options.push(graphVisualizationButton);
}

View file

@ -28,6 +28,11 @@ import { useInvestigateInTimeline } from '../../../../detections/components/aler
import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { useGraphPreview } from '../../shared/hooks/use_graph_preview';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { createUseUiSetting$Mock } from '../../../../common/lib/kibana/kibana_react.mock';
import {
ENABLE_GRAPH_VISUALIZATION_SETTING,
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING,
} from '../../../../../common/constants';
jest.mock('../hooks/use_expand_section');
jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({
@ -59,13 +64,14 @@ jest.mock('../../../../common/hooks/use_experimental_features', () => ({
}));
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
const mockUseUiSetting = jest.fn().mockImplementation((key) => [false]);
const mockUseUiSetting = jest.fn().mockReturnValue([false]);
jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...original,
useUiSetting$: () => mockUseUiSetting(),
useUiSetting$: (...args: unknown[]) => mockUseUiSetting(...args),
};
});
jest.mock('../../shared/hooks/use_graph_preview');
@ -96,7 +102,7 @@ const renderVisualizationsSection = (contextValue = panelContextValue) =>
describe('<VisualizationsSection />', () => {
beforeEach(() => {
mockUseUiSetting.mockReturnValue([false]);
mockUseUiSetting.mockImplementation(() => [false]);
mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] });
mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
loading: false,
@ -139,8 +145,17 @@ describe('<VisualizationsSection />', () => {
});
(useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true);
(useExpandSection as jest.Mock).mockReturnValue(true);
mockUseUiSetting.mockReturnValue([false]);
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
mockUseUiSetting.mockImplementation((key, defaultValue) => {
const useUiSetting$Mock = createUseUiSetting$Mock();
if (key === ENABLE_GRAPH_VISUALIZATION_SETTING) {
return [false, jest.fn()];
} else if (key === ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING) {
return [false, jest.fn()];
}
return useUiSetting$Mock(key, defaultValue);
});
const { getByTestId, queryByTestId } = renderVisualizationsSection();
expect(getByTestId(VISUALIZATIONS_SECTION_CONTENT_TEST_ID)).toBeVisible();
@ -152,18 +167,36 @@ describe('<VisualizationsSection />', () => {
it('should render the graph preview component if the feature is enabled', () => {
(useExpandSection as jest.Mock).mockReturnValue(true);
mockUseUiSetting.mockReturnValue([true]);
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
mockUseUiSetting.mockImplementation((key, defaultValue) => {
const useUiSetting$Mock = createUseUiSetting$Mock();
if (key === ENABLE_GRAPH_VISUALIZATION_SETTING) {
return [true, jest.fn()];
} else if (key === ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING) {
return [true, jest.fn()];
}
return useUiSetting$Mock(key, defaultValue);
});
const { getByTestId } = renderVisualizationsSection();
expect(getByTestId(`${GRAPH_PREVIEW_TEST_ID}LeftSection`)).toBeInTheDocument();
});
it('should not render the graph preview component if the experimental feature is disabled', () => {
it('should not render the graph preview component if the graph feature is disabled', () => {
(useExpandSection as jest.Mock).mockReturnValue(true);
mockUseUiSetting.mockReturnValue([true]);
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
mockUseUiSetting.mockImplementation((key, defaultValue) => {
const useUiSetting$Mock = createUseUiSetting$Mock();
if (key === ENABLE_GRAPH_VISUALIZATION_SETTING) {
return [false, jest.fn()];
} else if (key === ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING) {
return [true, jest.fn()];
}
return useUiSetting$Mock(key, defaultValue);
});
const { queryByTestId } = renderVisualizationsSection();
@ -172,8 +205,17 @@ describe('<VisualizationsSection />', () => {
it('should not render the graph preview component if the flyout feature is disabled', () => {
(useExpandSection as jest.Mock).mockReturnValue(true);
mockUseUiSetting.mockReturnValue([false]);
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
mockUseUiSetting.mockImplementation((key, defaultValue) => {
const useUiSetting$Mock = createUseUiSetting$Mock();
if (key === ENABLE_GRAPH_VISUALIZATION_SETTING) {
return [true, jest.fn()];
} else if (key === ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING) {
return [false, jest.fn()];
}
return useUiSetting$Mock(key, defaultValue);
});
const { queryByTestId } = renderVisualizationsSection();

View file

@ -17,9 +17,10 @@ import { VISUALIZATIONS_TEST_ID } from './test_ids';
import { GraphPreviewContainer } from './graph_preview_container';
import { useDocumentDetailsContext } from '../../shared/context';
import { useGraphPreview } from '../../shared/hooks/use_graph_preview';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants';
import { GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE } from '../../shared/constants/experimental_features';
import {
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING,
ENABLE_GRAPH_VISUALIZATION_SETTING,
} from '../../../../../common/constants';
const KEY = 'visualizations';
@ -35,9 +36,7 @@ export const VisualizationsSection = memo(() => {
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING
);
const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled(
GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE
);
const [graphVisualizationEnabled] = useUiSetting$<boolean>(ENABLE_GRAPH_VISUALIZATION_SETTING);
// Decide whether to show the graph preview or not
const { hasGraphRepresentation } = useGraphPreview({
@ -47,7 +46,7 @@ export const VisualizationsSection = memo(() => {
});
const shouldShowGraphPreview =
visualizationInFlyoutEnabled && isGraphFeatureEnabled && hasGraphRepresentation;
visualizationInFlyoutEnabled && graphVisualizationEnabled && hasGraphRepresentation;
return (
<ExpandableSection

View file

@ -1,10 +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.
*/
/** This security solution experimental feature allows user to enable/disable the graph visualization in Flyout feature (depends on securitySolution:enableVisualizationsInFlyout) */
export const GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE =
'graphVisualizationInFlyoutEnabled' as const;

View file

@ -41,6 +41,7 @@ import {
EXCLUDE_COLD_AND_FROZEN_TIERS_IN_ANALYZER,
EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION,
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING,
ENABLE_GRAPH_VISUALIZATION_SETTING,
} from '../common/constants';
import type { ExperimentalFeatures } from '../common/experimental_features';
import { LogLevelSetting } from '../common/api/detection_engine/rule_monitoring';
@ -64,6 +65,12 @@ export const initUiSettings = (
experimentalFeatures: ExperimentalFeatures,
validationsEnabled: boolean
) => {
const enableVisualizationsInFlyoutLabel = i18n.translate(
'xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel',
{
defaultMessage: 'Enable visualizations in flyout',
}
);
const securityUiSettings: Record<string, UiSettingsParams<unknown>> = {
[DEFAULT_APP_REFRESH_INTERVAL]: {
type: 'json',
@ -201,9 +208,7 @@ export const initUiSettings = (
schema: schema.boolean(),
},
[ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING]: {
name: i18n.translate('xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel', {
defaultMessage: 'Enable visualizations in flyout',
}),
name: enableVisualizationsInFlyoutLabel,
value: false,
description: i18n.translate(
'xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription',
@ -218,6 +223,28 @@ export const initUiSettings = (
requiresPageReload: true,
schema: schema.boolean(),
},
[ENABLE_GRAPH_VISUALIZATION_SETTING]: {
name: i18n.translate('xpack.securitySolution.uiSettings.enableGraphVisualizationLabel', {
defaultMessage: 'Enable graph visualization',
}),
description: i18n.translate(
'xpack.securitySolution.uiSettings.enableGraphVisualizationDescription',
{
defaultMessage: `<em>[technical preview]</em> Enable the Graph Visualization feature within the Security Solution.
<br/>Note: This feature requires the {visualizationFlyoutFeatureFlag} setting to be enabled.
<br/>Please ensure both settings are enabled to use graph visualizations in flyout.`,
values: {
em: (chunks) => `<em>${chunks}</em>`,
visualizationFlyoutFeatureFlag: `<code>${enableVisualizationsInFlyoutLabel}</code>`,
},
}
),
type: 'boolean',
value: false,
category: [APP_ID],
requiresPageReload: true,
schema: schema.boolean(),
},
[DEFAULT_RULES_TABLE_REFRESH_SETTING]: {
name: i18n.translate('xpack.securitySolution.uiSettings.rulesTableRefresh', {
defaultMessage: 'Rules auto refresh',

View file

@ -45,6 +45,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
`--xpack.fleet.packages.0.name=cloud_security_posture`,
`--xpack.fleet.packages.0.version=${CLOUD_SECURITY_PLUGIN_VERSION}`,
// `--xpack.fleet.registryUrl=https://localhost:8080`,
// Enables /internal/cloud_security_posture/graph API
`--uiSettings.overrides.securitySolution:enableGraphVisualization=true`,
],
},
};

View file

@ -44,9 +44,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
* 2. merge the updated version number change to kibana
*/
`--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=true`,
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'graphVisualizationInFlyoutEnabled',
])}`,
`--uiSettings.overrides.securitySolution:enableGraphVisualization=true`,
`--xpack.fleet.packages.0.name=cloud_security_posture`,
`--xpack.fleet.packages.0.version=${CLOUD_SECURITY_PLUGIN_VERSION}`,
// `--xpack.fleet.registryUrl=https://localhost:8080`,

View file

@ -33,5 +33,7 @@ export default createTestConfig({
'--xpack.dataUsage.autoops.api.url=http://localhost:9000',
`--xpack.dataUsage.autoops.api.tls.certificate=${KBN_CERT_PATH}`,
`--xpack.dataUsage.autoops.api.tls.key=${KBN_KEY_PATH}`,
// Enables /internal/cloud_security_posture/graph API
`--uiSettings.overrides.securitySolution:enableGraphVisualization=true`,
],
});