From a982ad7577d99177998b48e9ae261922ea29b9cd Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 29 Oct 2024 19:42:24 +0000 Subject: [PATCH 01/36] Added node button to support node click to open a popover. --- .../src/components/node/diamond_node.tsx | 18 +-- .../src/components/node/ellipse_node.tsx | 19 +-- .../src/components/node/hexagon_node.tsx | 19 +-- .../components/node/node_expand_button.tsx | 43 +++++++ .../src/components/node/pentagon_node.tsx | 18 +-- .../src/components/node/rectangle_node.tsx | 18 +-- .../graph/src/components/node/styles.tsx | 110 +++++++++--------- .../graph/src/components/types.ts | 15 ++- 8 files changed, 167 insertions(+), 93 deletions(-) create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx index f96068061a433..413f824ea3eb7 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx @@ -15,16 +15,17 @@ import { NodeShapeOnHoverSvg, NodeShapeSvg, NodeIcon, - NodeButton, HandleStyleOverride, + NodeButton, } from './styles'; import { DiamondHoverShape, DiamondShape } from './shapes/diamond_shape'; +import { NodeExpandButton } from './node_expand_button'; const NODE_WIDTH = 99; const NODE_HEIGHT = 98; export const DiamondNode: React.FC = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -55,11 +56,14 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2 - 4}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 4}px`} + /> + )} = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -47,6 +48,7 @@ export const EllipseNode: React.FC = memo((props: NodeProps) => { viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg" + className="node-shape" > = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} + /> + )} = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -47,6 +48,7 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => { viewBox="0 0 71 78" fill="none" xmlns="http://www.w3.org/2000/svg" + className="node-shape" > = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2 + 2}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2 - 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2 + 2}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 2}px`} + /> + )} , unToggleCallback: () => void) => void; +} + +export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { + // State to track whether the icon is "plus" or "minus" + const [isToggled, setIsToggled] = useState(false); + + const unToggleCallback = useCallback(() => { + setIsToggled(false); + }, []); + + const onClickHandler = (e: React.MouseEvent) => { + setIsToggled(!isToggled); + onClick?.(e, unToggleCallback); + }; + + return ( + + + + ); +}; + +NodeExpandButton.ExpandButtonSize = ExpandButtonSize; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx index f2282e9fa2d7d..7a918d830f571 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx @@ -15,11 +15,12 @@ import { NodeShapeOnHoverSvg, NodeShapeSvg, NodeIcon, - NodeButton, HandleStyleOverride, + NodeButton, } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { PentagonHoverShape, PentagonShape } from './shapes/pentagon_shape'; +import { NodeExpandButton } from './node_expand_button'; const PentagonShapeOnHover = styled(NodeShapeOnHoverSvg)` transform: translate(-50%, -51.5%); @@ -29,7 +30,7 @@ const NODE_WIDTH = 91; const NODE_HEIGHT = 88; export const PentagonNode: React.FC = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -60,11 +61,14 @@ export const PentagonNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} + /> + )} = memo((props: NodeProps) => { - const { id, color, icon, label, interactive, expandButtonClick } = + const { id, color, icon, label, interactive, expandButtonClick, nodeClick } = props.data as EntityNodeViewModel; const { euiTheme } = useEuiTheme(); return ( @@ -55,11 +56,14 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => { {icon && } {interactive && ( - expandButtonClick?.(e, props)} - x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 4}px`} - y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize / 2) / 2}px`} - /> + <> + nodeClick?.(e, props)} /> + expandButtonClick?.(e, props, unToggleCallback)} + x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 4}px`} + y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize / 2) / 2}px`} + /> + )} ) => void; +} + +export const NodeButton: React.FC = ({ onClick }) => ( + + + +); + +const StyledNodeContainer = styled.div` + position: absolute; + width: ${NODE_WIDTH}px; + height: ${NODE_HEIGHT}px; + z-index: 1; +`; + +const StyledNodeButton = styled.div` + width: ${NODE_WIDTH}px; + height: ${NODE_HEIGHT}px; +`; + +export const StyledNodeExpandButton = styled.div` + opacity: 0; /* Hidden by default */ + transition: opacity 0.2s ease; /* Smooth transition */ + ${(props: NodeExpandButtonProps) => + (Boolean(props.x) || Boolean(props.y)) && + `transform: translate(${props.x ?? '0'}, ${props.y ?? '0'});`} + position: absolute; + z-index: 1; + + &.toggled { + opacity: 1; + } + + ${NodeShapeContainer}:hover & { + opacity: 1; /* Show on hover */ + } + + &:has(button:focus) { + opacity: 1; /* Show when button is active */ + } + + .react-flow__node:focus:focus-visible & { + opacity: 1; /* Show on node focus */ + } +`; + export const NodeShapeOnHoverSvg = styled(NodeShapeSvg)` opacity: 0; /* Hidden by default */ transition: opacity 0.2s ease; /* Smooth transition */ @@ -108,6 +157,10 @@ export const NodeShapeOnHoverSvg = styled(NodeShapeSvg)` opacity: 1; /* Show on hover */ } + ${NodeShapeContainer}:has(${StyledNodeExpandButton}.toggled) & { + opacity: 1; /* Show on hover */ + } + .react-flow__node:focus:focus-visible & { opacity: 1; /* Show on hover */ } @@ -145,9 +198,9 @@ NodeLabel.defaultProps = { textAlign: 'center', }; -const ExpandButtonSize = 18; +export const ExpandButtonSize = 18; -const RoundEuiButtonIcon = styled(EuiButtonIcon)` +export const RoundEuiButtonIcon = styled(EuiButtonIcon)` border-radius: 50%; background-color: ${(_props) => useEuiBackgroundColor('plain')}; width: ${ExpandButtonSize}px; @@ -164,57 +217,6 @@ const RoundEuiButtonIcon = styled(EuiButtonIcon)` } `; -export const StyledNodeButton = styled.div` - opacity: 0; /* Hidden by default */ - transition: opacity 0.2s ease; /* Smooth transition */ - ${(props: NodeButtonProps) => - (Boolean(props.x) || Boolean(props.y)) && - `transform: translate(${props.x ?? '0'}, ${props.y ?? '0'});`} - position: absolute; - z-index: 1; - - ${NodeShapeContainer}:hover & { - opacity: 1; /* Show on hover */ - } - - &:has(button:focus) { - opacity: 1; /* Show when button is active */ - } - - .react-flow__node:focus:focus-visible & { - opacity: 1; /* Show on node focus */ - } -`; - -export interface NodeButtonProps { - x?: string; - y?: string; - onClick?: (e: React.MouseEvent) => void; -} - -export const NodeButton = ({ x, y, onClick }: NodeButtonProps) => { - // State to track whether the icon is "plus" or "minus" - const [isToggled, setIsToggled] = useState(false); - - const onClickHandler = (e: React.MouseEvent) => { - setIsToggled(!isToggled); - onClick?.(e); - }; - - return ( - - - - ); -}; - -NodeButton.ExpandButtonSize = ExpandButtonSize; - export const HandleStyleOverride: React.CSSProperties = { background: 'none', border: 'none', diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts index 27ec18f35f45b..2c33bef1cb8ec 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/types.ts @@ -24,11 +24,20 @@ interface BaseNodeDataViewModel { interactive?: boolean; } +export type NodeClickCallback = (e: React.MouseEvent, node: NodeProps) => void; + +export type ExpandButtonClickCallback = ( + e: React.MouseEvent, + node: NodeProps, + unToggleCallback: () => void +) => void; + export interface EntityNodeViewModel extends Record, EntityNodeDataModel, BaseNodeDataViewModel { - expandButtonClick?: (e: React.MouseEvent, node: NodeProps) => void; + expandButtonClick?: ExpandButtonClickCallback; + nodeClick?: NodeClickCallback; } export interface GroupNodeViewModel @@ -39,9 +48,7 @@ export interface GroupNodeViewModel export interface LabelNodeViewModel extends Record, LabelNodeDataModel, - BaseNodeDataViewModel { - expandButtonClick?: (e: React.MouseEvent, node: NodeProps) => void; -} + BaseNodeDataViewModel {} export type NodeViewModel = EntityNodeViewModel | GroupNodeViewModel | LabelNodeViewModel; From c94c04aeb2c52db63c6c99ee0cb9e511584dff1b Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 29 Oct 2024 19:43:33 +0000 Subject: [PATCH 02/36] Fixed SvgDefsMarker so it won't capture clicks --- .../graph/src/components/edge/styles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx index 8f7c5e29ec3fe..58ecea3bc3bea 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx @@ -128,7 +128,7 @@ export const SvgDefsMarker = () => { const { euiTheme } = useEuiTheme(); return ( - + From 5c00b6eb9ffcb1976d4590cdb42a4c97416c2706 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 29 Oct 2024 19:51:44 +0000 Subject: [PATCH 03/36] Fixed missing padding when lazy loading the graph --- .../right/components/graph_preview.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx index c3c6d65c7e986..efe0a861781b8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { memo, useMemo } from 'react'; -import { EuiSkeletonText } from '@elastic/eui'; +import { EuiPanel, EuiSkeletonText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -77,7 +77,13 @@ export const GraphPreview: React.FC = memo( defaultMessage="An error is preventing this alert from being visualized." /> ) : ( - }> + + + + } + > Date: Wed, 30 Oct 2024 01:04:54 +0000 Subject: [PATCH 04/36] Added graph visualization tab with expand capability --- .../graph/src/components/graph/graph.tsx | 83 ++++--- .../src/components/graph/graph_popover.tsx | 56 +++++ .../components/graph/use_graph_popover.tsx | 57 +++++ .../src/components/graph_popovers.stories.tsx | 218 ++++++++++++++++++ .../graph/src/components/index.ts | 2 + .../graph/src/components/styles.tsx | 71 ++++++ .../components/graph_node_expand_popover.tsx | 103 +++++++++ .../left/components/graph_visualization.tsx | 177 ++++++++++++++ .../left/components/test_ids.ts | 1 + .../use_graph_node_expand_popover.tsx | 128 ++++++++++ .../document_details/left/tabs/test_ids.ts | 2 + .../left/tabs/visualize_tab.tsx | 29 ++- .../components/graph_preview_container.tsx | 43 +++- .../right/hooks/use_fetch_graph_data.ts | 36 ++- ...e_navigate_to_graph_visualization.test.tsx | 94 ++++++++ .../use_navigate_to_graph_visualization.tsx | 104 +++++++++ 16 files changed, 1162 insertions(+), 42 deletions(-) create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index eca9872d73897..ff4cc2dc64757 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { useMemo, useRef, useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; +import { size, isEmpty, isEqual, xorWith } from 'lodash'; import { Background, Controls, @@ -33,9 +34,23 @@ import type { EdgeViewModel, NodeViewModel } from '../types'; import '@xyflow/react/dist/style.css'; export interface GraphProps extends CommonProps { + /** + * Array of node view models to be rendered in the graph. + */ nodes: NodeViewModel[]; + /** + * Array of edge view models to be rendered in the graph. + */ edges: EdgeViewModel[]; + /** + * Determines whether the graph is interactive (allows panning, zooming, etc.). + * When set to false, the graph is locked and user interactions are disabled, effectively putting it in view-only mode. + */ interactive: boolean; + /** + * Determines whether the graph is locked. Nodes and edges are still interactive, but the graph itself is not. + */ + isLocked?: boolean; } const nodeTypes = { @@ -66,28 +81,39 @@ const edgeTypes = { * * @returns {JSX.Element} The rendered Graph component. */ -export const Graph: React.FC = ({ nodes, edges, interactive, ...rest }) => { - const layoutCalled = useRef(false); - const [isGraphLocked, setIsGraphLocked] = useState(interactive); - const { initialNodes, initialEdges } = useMemo( - () => processGraph(nodes, edges, isGraphLocked), - [nodes, edges, isGraphLocked] - ); - - const [nodesState, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edgesState, _setEdges, onEdgesChange] = useEdgesState(initialEdges); - - if (!layoutCalled.current) { - const { nodes: layoutedNodes } = layoutGraph(nodesState, edgesState); - setNodes(layoutedNodes); - layoutCalled.current = true; - } +export const Graph: React.FC = ({ + nodes, + edges, + interactive, + isLocked: isLockedProp = false, + ...rest +}) => { + const backgroundId = Math.random().toFixed(4); // TODO: use useId(); when available (react >=18) + const [prevNodes, setPrevNodes] = useState([]); + const [prevEdges, setPrevEdges] = useState([]); + const [isGraphLocked, setIsGraphLocked] = useState(!interactive); + const [nodesState, setNodes, onNodesChange] = useNodesState>([]); + const [edgesState, setEdges, onEdgesChange] = useEdgesState>([]); + + useEffect(() => { + // On nodes or edges changes reset the graph and re-layout + if (!isArrayOfObjectsEqual(nodes, prevNodes) || !isArrayOfObjectsEqual(edges, prevEdges)) { + const { initialNodes, initialEdges } = processGraph(nodes, edges, !isGraphLocked); + const { nodes: layoutedNodes } = layoutGraph(initialNodes, initialEdges); + + setNodes(layoutedNodes); + setEdges(initialEdges); + setPrevNodes(nodes); + setPrevEdges(edges); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when nodes or edges change + }, [nodes, edges, setNodes, setEdges]); const onInteractiveStateChange = useCallback( (interactiveStatus: boolean): void => { - setIsGraphLocked(interactiveStatus); - setNodes((prevNodes) => - prevNodes.map((node) => ({ + setIsGraphLocked(!interactiveStatus); + setNodes((currNodes) => + currNodes.map((node) => ({ ...node, data: { ...node.data, @@ -123,16 +149,16 @@ export const Graph: React.FC = ({ nodes, edges, interactive, ...rest onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} proOptions={{ hideAttribution: true }} - panOnDrag={isGraphLocked} - zoomOnScroll={isGraphLocked} - zoomOnPinch={isGraphLocked} - zoomOnDoubleClick={isGraphLocked} - preventScrolling={isGraphLocked} - nodesDraggable={interactive && isGraphLocked} + panOnDrag={!isGraphLocked && !isLockedProp} + zoomOnScroll={!isGraphLocked && !isLockedProp} + zoomOnPinch={!isGraphLocked && !isLockedProp} + zoomOnDoubleClick={!isGraphLocked && !isLockedProp} + preventScrolling={!interactive} + nodesDraggable={interactive && !isGraphLocked && !isLockedProp} maxZoom={1.3} > {interactive && } - + ); @@ -202,3 +228,6 @@ const processGraph = ( return { initialNodes, initialEdges }; }; + +const isArrayOfObjectsEqual = (x: object[], y: object[]) => + size(x) === size(y) && isEmpty(xorWith(x, y, isEqual)); diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx new file mode 100644 index 0000000000000..d06b1b76f80ce --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -0,0 +1,56 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import type { CommonProps, EuiWrappingPopoverProps } from '@elastic/eui'; +import { EuiWrappingPopover } from '@elastic/eui'; + +export interface GraphPopoverProps + extends PropsWithChildren, + CommonProps, + Pick< + EuiWrappingPopoverProps, + 'anchorPosition' | 'panelClassName' | 'panelPaddingSize' | 'panelStyle' | 'panelProps' + > { + isOpen: boolean; + anchorElement: HTMLElement | null; + closePopover: () => void; +} + +export const GraphPopover: React.FC = ({ + isOpen, + anchorElement, + closePopover, + children, + ...rest +}) => { + if (!anchorElement) { + return null; + } + + return ( + { + closePopover(); + }, + }} + > + {children} + + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx new file mode 100644 index 0000000000000..f5bca30d1e5ae --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx @@ -0,0 +1,57 @@ +/* + * 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 { useCallback, useMemo, useState } from 'react'; + +export interface PopoverActions { + openPopover: (anchorElement: HTMLElement) => void; + closePopover: () => void; +} + +export interface PopoverState { + isOpen: boolean; + anchorElement: HTMLElement | null; +} + +export interface GraphPopoverState { + id: string; + actions: PopoverActions; + state: PopoverState; +} + +export const useGraphPopover = (id: string): GraphPopoverState => { + const [isOpen, setIsOpen] = useState(false); + const [anchorElement, setAnchorElement] = useState(null); + + // Memoize actions to prevent them from changing on re-renders + const openPopover = useCallback((anchor: HTMLElement) => { + setAnchorElement(anchor); + setIsOpen(true); + }, []); + + const closePopover = useCallback(() => { + setIsOpen(false); + setAnchorElement(null); + }, []); + + // Memoize the context values + const actions: PopoverActions = useMemo( + () => ({ openPopover, closePopover }), + [openPopover, closePopover] + ); + + const state: PopoverState = useMemo(() => ({ isOpen, anchorElement }), [isOpen, anchorElement]); + + return useMemo( + () => ({ + id, + actions, + state, + }), + [id, actions, state] + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx new file mode 100644 index 0000000000000..87bdea248ed5c --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx @@ -0,0 +1,218 @@ +/* + * 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, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ThemeProvider } from '@emotion/react'; +import { Story } from '@storybook/react'; +import { css } from '@emotion/react'; +import { EuiListGroup, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; +import type { EntityNodeViewModel, NodeProps } from '.'; +import { Graph } from '.'; +import { GraphPopover } from './graph/graph_popover'; +import { ExpandButtonClickCallback } from './types'; +import { useGraphPopover } from './graph/use_graph_popover'; +import { ExpandPopoverListItem } from './styles'; + +export default { + title: 'Components/Graph Components/Graph Popovers', + description: 'CDR - Graph visualization', + argTypes: {}, +}; + +const useExpandButtonPopover = () => { + const { id, state, actions } = useGraphPopover('node-expand-popover'); + const { openPopover, closePopover } = actions; + + const selectedNode = useRef(null); + const unToggleCallbackRef = useRef<(() => void) | null>(null); + const [pendingOpen, setPendingOpen] = useState<{ + node: NodeProps; + el: HTMLElement; + unToggleCallback: () => void; + } | null>(null); + + const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( + (e, node, unToggleCallback) => { + if (selectedNode.current?.id === node.id) { + // If the same node is clicked again, close the popover + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + } else { + // Close the current popover if open + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + + // Set the pending open state + setPendingOpen({ node, el: e.currentTarget, unToggleCallback }); + + closePopover(); + } + }, + [closePopover] + ); + + useEffect(() => { + if (!state.isOpen && pendingOpen) { + const { node, el, unToggleCallback } = pendingOpen; + + selectedNode.current = node; + unToggleCallbackRef.current = unToggleCallback; + openPopover(el); + + setPendingOpen(null); + } + }, [state.isOpen, pendingOpen, openPopover]); + + const closePopoverHandler = useCallback(() => { + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + }, [closePopover]); + + const { euiTheme } = useEuiTheme(); + + const PopoverComponent = memo(() => ( + + + {}} + /> + {}} + /> + {}} + /> + + {}} /> + + + )); + + const actionsWithClose = useMemo( + () => ({ + ...actions, + closePopover: closePopoverHandler, + }), + [actions, closePopoverHandler] + ); + + return useMemo( + () => ({ + onNodeExpandButtonClick, + Popover: PopoverComponent, + id, + actions: actionsWithClose, + state, + }), + [PopoverComponent, actionsWithClose, id, onNodeExpandButtonClick, state] + ); +}; + +const useNodePopover = () => { + const { id, state, actions } = useGraphPopover('node-popover'); + + const PopoverComponent = memo(() => ( + + TODO + + )); + + return useMemo( + () => ({ + onNodeClick: (e: React.MouseEvent) => actions.openPopover(e.currentTarget), + Popover: PopoverComponent, + id, + actions, + state, + }), + [PopoverComponent, actions, id, state] + ); +}; + +const Template: Story = () => { + const expandNodePopover = useExpandButtonPopover(); + const nodePopover = useNodePopover(); + const popovers = [expandNodePopover, nodePopover]; + const isPopoverOpen = popovers.some((popover) => popover.state.isOpen); + + const popoverOpenWrapper = (cb: Function, ...args: any[]) => { + [expandNodePopover.actions.closePopover, nodePopover.actions.closePopover].forEach( + (closePopover) => { + closePopover(); + } + ); + cb.apply(null, args); + }; + + const expandButtonClickHandler = (...args: any[]) => + popoverOpenWrapper(expandNodePopover.onNodeExpandButtonClick, ...args); + const nodeClickHandler = (...args: any[]) => popoverOpenWrapper(nodePopover.onNodeClick, ...args); + + const nodes: EntityNodeViewModel[] = useMemo( + () => + (['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const).map((shape, idx) => ({ + id: `${idx}`, + label: `Node ${idx}`, + color: 'primary', + icon: 'okta', + interactive: true, + shape, + expandButtonClick: expandButtonClickHandler, + nodeClick: nodeClickHandler, + })), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( + + + {popovers?.map((popover) => popover.Popover && )} + + ); +}; + +export const GraphPopovers = Template.bind({}); diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts index 5b2f8d71323bb..2b050aa55429f 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts @@ -6,6 +6,8 @@ */ export { Graph } from './graph/graph'; +export { GraphPopover } from './graph/graph_popover'; +export { useGraphPopover } from './graph/use_graph_popover'; export type { GraphProps } from './graph/graph'; export type { NodeViewModel, diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx new file mode 100644 index 0000000000000..8033f7ddd8d66 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx @@ -0,0 +1,71 @@ +/* + * 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 { + EuiIcon, + useEuiBackgroundColor, + useEuiTheme, + type EuiIconProps, + type _EuiBackgroundColor, + EuiListGroupItemProps, + EuiListGroupItem, + EuiText, +} from '@elastic/eui'; +import styled from '@emotion/styled'; + +const IconContainer = styled.div` + position: relative; + width: 24px; + height: 24px; + border-radius: 50%; + color: ${(props) => { + const { euiTheme } = useEuiTheme(); + return euiTheme.colors[props.color as keyof typeof euiTheme.colors] as string; + }}; + background-color: ${(props) => useEuiBackgroundColor(props.color as _EuiBackgroundColor)}; + border: 1px solid + ${(props) => { + const { euiTheme } = useEuiTheme(); + return euiTheme.colors[props.color as keyof typeof euiTheme.colors] as string; + }}; + margin-right: 8px; +`; + +const StyleEuiIcon = styled(EuiIcon)` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +const RoundedEuiIcon: React.FC = ({ ...rest }) => ( + + + +); + +export const ExpandPopoverListItem: React.FC< + Pick +> = (props) => { + const { euiTheme } = useEuiTheme(); + return ( + + ) : undefined + } + label={ + + {props.label} + + } + onClick={props.onClick} + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx new file mode 100644 index 0000000000000..93450b644f721 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx @@ -0,0 +1,103 @@ +/* + * 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, { memo } from 'react'; +import { GraphPopover } from '@kbn/cloud-security-posture-graph'; +import { EuiHorizontalRule, EuiListGroup, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ExpandPopoverListItem } from '@kbn/cloud-security-posture-graph/src/components/styles'; +import { css } from '@emotion/react'; + +interface GraphNodeExpandPopoverProps { + isOpen: boolean; + anchorElement: HTMLElement | null; + closePopover: () => void; + onExploreRelatedEntitiesClick?: () => void; + onShowActionsByEntityClick?: () => void; + onShowActionsOnEntityClick?: () => void; + onViewEntityDetailsClick?: () => void; +} + +export const GraphNodeExpandPopover: React.FC = memo( + ({ + isOpen, + anchorElement, + closePopover, + onExploreRelatedEntitiesClick, + onShowActionsByEntityClick, + onShowActionsOnEntityClick, + onViewEntityDetailsClick, + }) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {onExploreRelatedEntitiesClick && ( + + )} + {onShowActionsByEntityClick && ( + + )} + {onShowActionsOnEntityClick && ( + + )} + {onViewEntityDetailsClick && ( + <> + + + + )} + + + ); + } +); + +GraphNodeExpandPopover.displayName = 'GraphNodeExpandPopover'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx new file mode 100644 index 0000000000000..480e6bdfa3bc2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -0,0 +1,177 @@ +/* + * 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, { memo, useMemo, useState } from 'react'; +import { SearchBar } from '@kbn/unified-search-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { NodeViewModel } from '@kbn/cloud-security-posture-graph'; +import type { Query } from '@kbn/es-query'; +import { css } from '@emotion/css'; +import { useDocumentDetailsContext } from '../../shared/context'; +import { GRAPH_VISUALIZATION_TEST_ID } from './test_ids'; +import { useFetchGraphData } from '../../right/hooks/use_fetch_graph_data'; +import { useGraphPreview } from '../../right/hooks/use_graph_preview'; +import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; + +export const GRAPH_VISUALIZATION_ID = 'graph_visualization'; +const DEFAULT_FROM = 'now-60d/d'; +const DEFAULT_TO = 'now/d'; + +const GraphLazy = React.lazy(() => + import('@kbn/cloud-security-posture-graph').then((module) => ({ default: module.Graph })) +); + +const useGraphFilters = () => { + const [filters, setFilters] = useState({ + from: DEFAULT_FROM, + to: DEFAULT_TO, + }); + + const updateFilters = (newFilters: Partial) => { + setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); + }; + + return { filters, updateFilters }; +}; + +const useGraphData = ( + actorIds: Set, + eventIds: string[], + filters: { from: string; to: string } +) => { + const { data, refresh, isFetching } = useFetchGraphData({ + req: { + query: { + actorIds: Array.from(actorIds), + eventIds, + start: filters.from, + end: filters.to, + }, + }, + options: { + refetchOnWindowFocus: false, + keepPreviousData: true, + }, + }); + + return { data, refresh, isFetching }; +}; + +const useGraphPopovers = (setActorIds: React.Dispatch>>) => { + const { + services: { notifications }, + } = useKibana(); + + const nodeExpandPopover = useGraphNodeExpandPopover({ + onExploreRelatedEntitiesClick: (node) => { + notifications?.toasts.addInfo('Explore related entities is not implemented yet'); + }, + onShowActionsByEntityClick: (node) => { + setActorIds((prev) => new Set([...prev, node.id])); + }, + onShowActionsOnEntityClick: (node) => { + notifications?.toasts.addInfo('Show actions on entity is not implemented yet'); + }, + onViewEntityDetailsClick: (node) => { + notifications?.toasts.addInfo('View entity details is not implemented yet'); + }, + }); + + const popovers = [nodeExpandPopover]; + const popoverOpenWrapper = (cb: Function, ...args: unknown[]) => { + popovers.forEach(({ actions: { closePopover } }) => { + closePopover(); + }); + cb(...args); + }; + + return { nodeExpandPopover, popoverOpenWrapper }; +}; + +const useGraphNodes = ( + nodes: NodeViewModel[], + expandButtonClickHandler: (...args: unknown[]) => void +) => { + return useMemo(() => { + return nodes.map((node) => { + const nodeHandlers = + node.shape !== 'label' && node.shape !== 'group' + ? { + expandButtonClick: expandButtonClickHandler, + } + : undefined; + return { ...node, ...nodeHandlers }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodes]); +}; + +/** + * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab + */ +export const GraphVisualization: React.FC = memo(() => { + const { filters, updateFilters } = useGraphFilters(); + const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); + const { eventIds } = useGraphPreview({ + getFieldsData, + ecsData: dataAsNestedObject, + }); + + const [actorIds, setActorIds] = useState(() => new Set()); + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds); + const expandButtonClickHandler = (...args: unknown[]) => + popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); + const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); + const { data, refresh, isFetching } = useGraphData(actorIds, eventIds, filters); + const nodes = useGraphNodes(data?.nodes ?? [], expandButtonClickHandler); + + return ( +
+ + {...{ + appName: 'graph-visualization', + intl: null, + showFilterBar: false, + showDatePicker: true, + showAutoRefreshOnly: false, + showSaveQuery: false, + showQueryInput: false, + isLoading: isFetching, + isAutoRefreshDisabled: true, + dateRangeFrom: filters.from.split('/')[0], + dateRangeTo: filters.to.split('/')[0], + query: { query: '', language: 'kuery' }, + filters: [], + submitButtonStyle: 'iconOnly', + onQuerySubmit: (payload, isUpdate) => { + if (isUpdate) { + updateFilters({ from: payload.dateRange.from, to: payload.dateRange.to }); + setActorIds(new Set()); + } else { + refresh(); + } + }, + }} + /> + + + + +
+ ); +}); + +GraphVisualization.displayName = 'GraphVisualization'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index 8669b504f6861..6979fa9cfa053 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -11,6 +11,7 @@ import { PREFIX } from '../../../shared/test_ids'; export const ANALYZER_GRAPH_TEST_ID = `${PREFIX}AnalyzerGraph` as const; export const SESSION_VIEW_TEST_ID = `${PREFIX}SessionView` as const; +export const GRAPH_VISUALIZATION_TEST_ID = `${PREFIX}GraphVisualization` as const; /* Insights tab */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx new file mode 100644 index 0000000000000..7ae2d331f3343 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx @@ -0,0 +1,128 @@ +/* + * 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 { useGraphPopover } from '@kbn/cloud-security-posture-graph'; +import type { + ExpandButtonClickCallback, + NodeProps, +} from '@kbn/cloud-security-posture-graph/src/components/types'; +import type { PopoverActions } from '@kbn/cloud-security-posture-graph/src/components/graph/use_graph_popover'; +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { GraphNodeExpandPopover } from './graph_node_expand_popover'; + +interface UseGraphNodeExpandPopoverArgs { + onExploreRelatedEntitiesClick?: (node: NodeProps) => void; + onShowActionsByEntityClick?: (node: NodeProps) => void; + onShowActionsOnEntityClick?: (node: NodeProps) => void; + onViewEntityDetailsClick?: (node: NodeProps) => void; +} + +export const useGraphNodeExpandPopover = (args: UseGraphNodeExpandPopoverArgs = {}) => { + const { + onExploreRelatedEntitiesClick, + onShowActionsByEntityClick, + onShowActionsOnEntityClick, + onViewEntityDetailsClick, + } = args; + const { id, state, actions } = useGraphPopover('node-expand-popover'); + const { openPopover, closePopover } = actions; + + const selectedNode = useRef(null); + const unToggleCallbackRef = useRef<(() => void) | null>(null); + const [pendingOpen, setPendingOpen] = useState<{ + node: NodeProps; + el: HTMLElement; + unToggleCallback: () => void; + } | null>(null); + + const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( + (e, node, unToggleCallback) => { + if (selectedNode.current?.id === node.id) { + // If the same node is clicked again, close the popover + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + } else { + // Close the current popover if open + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + + // Set the pending open state + setPendingOpen({ node, el: e.currentTarget, unToggleCallback }); + + closePopover(); + } + }, + [closePopover] + ); + + useEffect(() => { + if (!state.isOpen && pendingOpen) { + const { node, el, unToggleCallback } = pendingOpen; + + selectedNode.current = node; + unToggleCallbackRef.current = unToggleCallback; + openPopover(el); + + setPendingOpen(null); + } + }, [state.isOpen, pendingOpen, openPopover]); + + const closePopoverHandler = useCallback(() => { + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + }, [closePopover]); + + const PopoverComponent = memo(() => ( + { + onExploreRelatedEntitiesClick?.(selectedNode.current as NodeProps); + closePopoverHandler(); + }} + onShowActionsByEntityClick={() => { + onShowActionsByEntityClick?.(selectedNode.current as NodeProps); + closePopoverHandler(); + }} + onShowActionsOnEntityClick={() => { + onShowActionsOnEntityClick?.(selectedNode.current as NodeProps); + closePopoverHandler(); + }} + onViewEntityDetailsClick={() => { + onViewEntityDetailsClick?.(selectedNode.current as NodeProps); + closePopoverHandler(); + }} + /> + )); + + PopoverComponent.displayName = GraphNodeExpandPopover.displayName; + + const actionsWithClose: PopoverActions = useMemo( + () => ({ + ...actions, + closePopover: closePopoverHandler, + }), + [actions, closePopoverHandler] + ); + + return useMemo( + () => ({ + onNodeExpandButtonClick, + PopoverComponent, + id, + actions: actionsWithClose, + state, + }), + [PopoverComponent, actionsWithClose, id, onNodeExpandButtonClick, state] + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/test_ids.ts index 1e99fb63d18a5..ec38f9865ed64 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/test_ids.ts @@ -13,6 +13,8 @@ export const VISUALIZE_TAB_SESSION_VIEW_BUTTON_TEST_ID = `${VISUALIZE_TAB_TEST_ID}SessionViewButton` as const; export const VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID = `${VISUALIZE_TAB_TEST_ID}GraphAnalyzerButton` as const; +export const VISUALIZE_TAB_GRAPH_VISUALIZATION_BUTTON_TEST_ID = + `${VISUALIZE_TAB_TEST_ID}GraphVisualizationButton` as const; const INSIGHTS_TAB_TEST_ID = `${PREFIX}InsightsTab` as const; export const INSIGHTS_TAB_BUTTON_GROUP_TEST_ID = `${INSIGHTS_TAB_TEST_ID}ButtonGroup` as const; export const INSIGHTS_TAB_ENTITIES_BUTTON_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 0dad444ee6ece..6f3cdf16a19ad 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -17,6 +17,7 @@ import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_ke import { VISUALIZE_TAB_BUTTON_GROUP_TEST_ID, VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID, + VISUALIZE_TAB_GRAPH_VISUALIZATION_BUTTON_TEST_ID, VISUALIZE_TAB_SESSION_VIEW_BUTTON_TEST_ID, } from './test_ids'; import { @@ -27,6 +28,8 @@ import { import { SESSION_VIEW_ID, SessionView } from '../components/session_view'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; +import { GRAPH_VISUALIZATION_ID, GraphVisualization } from '../components/graph_visualization'; +import { useGraphPreview } from '../../right/hooks/use_graph_preview'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ { @@ -51,11 +54,22 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [ }, ]; +const graphVisualizationButton = { + id: GRAPH_VISUALIZATION_ID, + label: ( + + ), + 'data-test-subj': VISUALIZE_TAB_GRAPH_VISUALIZATION_BUTTON_TEST_ID, +}; + /** * Visualize view displayed in the document details expandable flyout left section */ export const VisualizeTab = memo(() => { - const { scopeId } = useDocumentDetailsContext(); + const { scopeId, getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); const { openPreviewPanel } = useExpandableFlyoutApi(); const panels = useExpandableFlyoutState(); const [activeVisualizationId, setActiveVisualizationId] = useState( @@ -86,6 +100,16 @@ export const VisualizeTab = memo(() => { } }, [panels.left?.path?.subTab]); + // Decide whether to show the graph preview or not + const { isAuditLog: isGraphPreviewEnabled } = useGraphPreview({ + getFieldsData, + ecsData: dataAsNestedObject, + }); + + const options = [...visualizeButtons]; + + if (isGraphPreviewEnabled) options.push(graphVisualizationButton); + return ( <> { defaultMessage: 'Visualize options', } )} - options={visualizeButtons} + options={options} idSelected={activeVisualizationId} onChange={(id) => onChangeCompressed(id)} buttonSize="compressed" @@ -107,6 +131,7 @@ export const VisualizeTab = memo(() => { {activeVisualizationId === SESSION_VIEW_ID && } {activeVisualizationId === ANALYZE_GRAPH_ID && } + {activeVisualizationId === GRAPH_VISUALIZATION_ID && } ); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 1bc6a8dd7e547..b345486ebbc43 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -8,11 +8,14 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { ExpandablePanel } from '@kbn/security-solution-common'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; +import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; import { GraphPreview } from './graph_preview'; import { useFetchGraphData } from '../hooks/use_fetch_graph_data'; import { useGraphPreview } from '../hooks/use_graph_preview'; +import { useNavigateToGraphVisualization } from '../../shared/hooks/use_navigate_to_graph_visualization'; const DEFAULT_FROM = 'now-60d/d'; const DEFAULT_TO = 'now/d'; @@ -21,7 +24,19 @@ const DEFAULT_TO = 'now/d'; * Graph preview under Overview, Visualizations. It shows a graph representation of entities. */ export const GraphPreviewContainer: React.FC = () => { - const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext(); + const { dataAsNestedObject, getFieldsData, eventId, indexName, scopeId } = + useDocumentDetailsContext(); + + const [visualizationInFlyoutEnabled] = useUiSetting$( + ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING + ); + + const { navigateToGraphVisualization } = useNavigateToGraphVisualization({ + eventId, + indexName, + isFlyoutOpen: true, + scopeId, + }); const { eventIds } = useGraphPreview({ getFieldsData, @@ -29,7 +44,7 @@ export const GraphPreviewContainer: React.FC = () => { }); // TODO: default start and end might not capture the original event - const graphFetchQuery = useFetchGraphData({ + const { isLoading, isError, data } = useFetchGraphData({ req: { query: { actorIds: [], @@ -38,6 +53,9 @@ export const GraphPreviewContainer: React.FC = () => { end: DEFAULT_TO, }, }, + options: { + refetchOnWindowFocus: false, + }, }); return ( @@ -49,22 +67,29 @@ export const GraphPreviewContainer: React.FC = () => { defaultMessage="Graph preview" /> ), - iconType: 'indexMapping', + iconType: visualizationInFlyoutEnabled ? 'arrowStart' : 'indexMapping', + ...(visualizationInFlyoutEnabled && { + link: { + callback: navigateToGraphVisualization, + tooltip: ( + + ), + }, + }), }} data-test-subj={GRAPH_PREVIEW_TEST_ID} content={ - !graphFetchQuery.isLoading && !graphFetchQuery.isError + !isLoading && !isError ? { paddingSize: 'none', } : undefined } > - + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts index 2304cfb8d4fd2..ada59030eb25e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { GraphRequest, GraphResponse, @@ -30,6 +30,16 @@ export interface UseFetchGraphDataParams { * Defaults to true. */ enabled?: boolean; + /** + * If true, the query will refetch on window focus. + * Defaults to true. + */ + refetchOnWindowFocus?: boolean; + /** + * If true, the query will keep previous data till new data received. + * Defaults to false. + */ + keepPreviousData?: boolean; }; } @@ -41,6 +51,10 @@ export interface UseFetchGraphDataResult { * Indicates if the query is currently loading. */ isLoading: boolean; + /** + * Indicates if the query is currently fetching. + */ + isFetching: boolean; /** * Indicates if there was an error during the query. */ @@ -49,6 +63,10 @@ export interface UseFetchGraphDataResult { * The data returned from the query. */ data?: GraphResponse; + /** + * Function to manually refresh the query. + */ + refresh: () => void; } /** @@ -61,23 +79,33 @@ export const useFetchGraphData = ({ req, options, }: UseFetchGraphDataParams): UseFetchGraphDataResult => { + const queryClient = useQueryClient(); const { actorIds, eventIds, start, end } = req.query; const http = useHttp(); + const QUERY_KEY = ['useFetchGraphData', actorIds, eventIds, start, end]; - const { isLoading, isError, data } = useQuery( - ['useFetchGraphData', actorIds, eventIds, start, end], + const { isLoading, isError, data, isFetching } = useQuery( + QUERY_KEY, () => { return http.post(EVENT_GRAPH_VISUALIZATION_API, { version: '1', body: JSON.stringify(req), }); }, - options + { + enabled: options?.enabled ?? true, + refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, + keepPreviousData: options?.keepPreviousData ?? false, + } ); return { isLoading, + isFetching, isError, data, + refresh: () => { + queryClient.invalidateQueries(QUERY_KEY); + }, }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx new file mode 100644 index 0000000000000..c4c362250a1ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx @@ -0,0 +1,94 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { mockFlyoutApi } from '../mocks/mock_flyout_context'; +import { useWhichFlyout } from './use_which_flyout'; +import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; +import { useKibana } from '../../../../common/lib/kibana'; +import { DocumentDetailsRightPanelKey, DocumentDetailsLeftPanelKey } from '../constants/panel_keys'; +import { GRAPH_VISUALIZATION_ID } from '../../left/components/graph_visualization'; +import { useNavigateToGraphVisualization } from './use_navigate_to_graph_visualization'; + +jest.mock('@kbn/expandable-flyout'); +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_which_flyout'); + +const mockedUseKibana = mockUseKibana(); +(useKibana as jest.Mock).mockReturnValue(mockedUseKibana); + +const mockUseWhichFlyout = useWhichFlyout as jest.Mock; +const FLYOUT_KEY = 'SecuritySolution'; + +const eventId = 'eventId1'; +const indexName = 'index1'; +const scopeId = 'scopeId1'; + +describe('useNavigateToGraphVisualization', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + + it('when isFlyoutOpen is true, should return callback that opens left and preview panels', () => { + mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY); + const hookResult = renderHook(() => + useNavigateToGraphVisualization({ isFlyoutOpen: true, eventId, indexName, scopeId }) + ); + + // Act + hookResult.result.current.navigateToGraphVisualization(); + + expect(mockFlyoutApi.openLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { + tab: 'visualize', + subTab: GRAPH_VISUALIZATION_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }); + }); + + it('when isFlyoutOpen is false and scopeId is not timeline, should return callback that opens a new flyout', () => { + mockUseWhichFlyout.mockReturnValue(null); + + const hookResult = renderHook(() => + useNavigateToGraphVisualization({ isFlyoutOpen: false, eventId, indexName, scopeId }) + ); + + // Act + hookResult.result.current.navigateToGraphVisualization(); + + expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName, + scopeId, + }, + }, + left: { + id: DocumentDetailsLeftPanelKey, + path: { + tab: 'visualize', + subTab: GRAPH_VISUALIZATION_ID, + }, + params: { + id: eventId, + indexName, + scopeId, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx new file mode 100644 index 0000000000000..6fed3ce51b22d --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx @@ -0,0 +1,104 @@ +/* + * 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 { useCallback, useMemo } from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import type { Maybe } from '@kbn/timelines-plugin/common/search_strategy/common'; +import { useKibana } from '../../../../common/lib/kibana'; +import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey } from '../constants/panel_keys'; +import { GRAPH_VISUALIZATION_ID } from '../../left/components/graph_visualization'; + +export interface UseNavigateToGraphVisualizationParams { + /** + * When flyout is already open, call open left panel only + * When flyout is not open, open a new flyout + */ + isFlyoutOpen: boolean; + /** + * Id of the document + */ + eventId: string; + /** + * Name of the index used in the parent's page + */ + indexName: Maybe | undefined; + /** + * Scope id of the page + */ + scopeId: string; +} + +export interface UseNavigateToGraphVisualizationResult { + /** + * Callback to open analyzer in visualize tab + */ + navigateToGraphVisualization: () => void; +} + +/** + * Hook that returns the a callback to navigate to the graph visualization in the flyout + */ +export const useNavigateToGraphVisualization = ({ + isFlyoutOpen, + eventId, + indexName, + scopeId, +}: UseNavigateToGraphVisualizationParams): UseNavigateToGraphVisualizationResult => { + const { telemetry } = useKibana().services; + const { openLeftPanel, openFlyout } = useExpandableFlyoutApi(); + + const right: FlyoutPanelProps = useMemo( + () => ({ + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName, + scopeId, + }, + }), + [eventId, indexName, scopeId] + ); + + const left: FlyoutPanelProps = useMemo( + () => ({ + id: DocumentDetailsLeftPanelKey, + params: { + id: eventId, + indexName, + scopeId, + }, + path: { + tab: 'visualize', + subTab: GRAPH_VISUALIZATION_ID, + }, + }), + [eventId, indexName, scopeId] + ); + + const navigateToGraphVisualization = useCallback(() => { + if (isFlyoutOpen) { + openLeftPanel(left); + telemetry.reportDetailsFlyoutTabClicked({ + location: scopeId, + panel: 'left', + tabId: 'visualize', + }); + } else { + openFlyout({ + right, + left, + }); + telemetry.reportDetailsFlyoutOpened({ + location: scopeId, + panel: 'left', + }); + } + }, [openFlyout, openLeftPanel, right, left, scopeId, telemetry, isFlyoutOpen]); + + return useMemo(() => ({ navigateToGraphVisualization }), [navigateToGraphVisualization]); +}; From 6f71b19b10cbec03a86772d70930b08f12136ea2 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Wed, 30 Oct 2024 17:53:46 +0000 Subject: [PATCH 05/36] checks feature flag before showing graph tab --- .../flyout/document_details/left/tabs/visualize_tab.tsx | 9 ++++++++- .../right/components/visualizations_section.tsx | 3 ++- .../document_details/shared/constants/feature_flags.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 6f3cdf16a19ad..fd810335e5256 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -11,6 +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 { useDocumentDetailsContext } from '../../shared/context'; import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; @@ -30,6 +31,7 @@ import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { GRAPH_VISUALIZATION_ID, GraphVisualization } from '../components/graph_visualization'; import { useGraphPreview } from '../../right/hooks/use_graph_preview'; +import { GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE } from '../../shared/constants/feature_flags'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ { @@ -100,6 +102,10 @@ export const VisualizeTab = memo(() => { } }, [panels.left?.path?.subTab]); + const graphVisualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( + GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE + ); + // Decide whether to show the graph preview or not const { isAuditLog: isGraphPreviewEnabled } = useGraphPreview({ getFieldsData, @@ -108,7 +114,8 @@ export const VisualizeTab = memo(() => { const options = [...visualizeButtons]; - if (isGraphPreviewEnabled) options.push(graphVisualizationButton); + if (graphVisualizationInFlyoutEnabled && isGraphPreviewEnabled) + options.push(graphVisualizationButton); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index c328036eece43..ef7d9c3ff9c56 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -17,6 +17,7 @@ import { GraphPreviewContainer } from './graph_preview_container'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useDocumentDetailsContext } from '../../shared/context'; import { useGraphPreview } from '../hooks/use_graph_preview'; +import { GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE } from '../../shared/constants/feature_flags'; const KEY = 'visualizations'; @@ -26,7 +27,7 @@ const KEY = 'visualizations'; export const VisualizationsSection = memo(() => { const expanded = useExpandSection({ title: KEY, defaultValue: false }); const graphVisualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( - 'graphVisualizationInFlyoutEnabled' + GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE ); const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext(); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts new file mode 100644 index 0000000000000..79a6479782ca1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE = + 'graphVisualizationInFlyoutEnabled' as const; From 3dc45232d1a403f38f4503ca19c0856c4f47b3bc Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Wed, 30 Oct 2024 17:54:45 +0000 Subject: [PATCH 06/36] Added filterbar support --- .../graph/src/components/graph/graph.tsx | 11 +++- .../src/components/graph/graph_popover.tsx | 6 ++- .../left/components/graph_visualization.tsx | 54 ++++++++++++++++--- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index ff4cc2dc64757..ded6cc868fd3d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import { size, isEmpty, isEqual, xorWith } from 'lodash'; import { Background, @@ -15,7 +15,7 @@ import { useEdgesState, useNodesState, } from '@xyflow/react'; -import type { Edge, Node } from '@xyflow/react'; +import type { Edge, FitViewOptions, Node } from '@xyflow/react'; import type { CommonProps } from '@elastic/eui'; import { SvgDefsMarker } from '../edge/styles'; import { @@ -89,6 +89,9 @@ export const Graph: React.FC = ({ ...rest }) => { const backgroundId = Math.random().toFixed(4); // TODO: use useId(); when available (react >=18) + const fitViewRef = useRef< + ((fitViewOptions?: FitViewOptions | undefined) => Promise) | null + >(null); const [prevNodes, setPrevNodes] = useState([]); const [prevEdges, setPrevEdges] = useState([]); const [isGraphLocked, setIsGraphLocked] = useState(!interactive); @@ -105,6 +108,9 @@ export const Graph: React.FC = ({ setEdges(initialEdges); setPrevNodes(nodes); setPrevEdges(edges); + setTimeout(() => { + fitViewRef.current?.(); + }, 0); } // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when nodes or edges change }, [nodes, edges, setNodes, setEdges]); @@ -132,6 +138,7 @@ export const Graph: React.FC = ({ fitView={true} onInit={(xyflow) => { window.requestAnimationFrame(() => xyflow.fitView()); + fitViewRef.current = xyflow.fitView; // When the graph is not initialized as interactive, we need to fit the view on resize if (!interactive) { diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx index d06b1b76f80ce..d1963458102a3 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -44,7 +44,11 @@ export const GraphPopover: React.FC = ({ disabled: false, crossFrame: true, noIsolation: false, - returnFocus: false, + returnFocus: (el) => { + anchorElement.focus(); + return false; + }, + preventScrollOnFocus: true, onClickOutside: () => { closePopover(); }, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index 480e6bdfa3bc2..fed990c3572ad 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -9,8 +9,10 @@ import React, { memo, useMemo, useState } from 'react'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { NodeViewModel } from '@kbn/cloud-security-posture-graph'; -import type { Query } from '@kbn/es-query'; +import type { Filter, Query } from '@kbn/es-query'; import { css } from '@emotion/css'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_VISUALIZATION_TEST_ID } from './test_ids'; import { useFetchGraphData } from '../../right/hooks/use_fetch_graph_data'; @@ -61,7 +63,10 @@ const useGraphData = ( return { data, refresh, isFetching }; }; -const useGraphPopovers = (setActorIds: React.Dispatch>>) => { +const useGraphPopovers = ( + setActorIds: React.Dispatch>>, + setSearchFilters: React.Dispatch> +) => { const { services: { notifications }, } = useKibana(); @@ -72,6 +77,29 @@ const useGraphPopovers = (setActorIds: React.Dispatch { setActorIds((prev) => new Set([...prev, node.id])); + setSearchFilters((prev) => [ + ...prev, + { + meta: { + disabled: false, + key: 'actor.entity.id', + field: 'actor.entity.id', + negate: false, + params: { + query: node.id, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'actor.entity.id': node.id, + }, + }, + $state: { + store: 'appState', + } as Filter['$state'], + }, + ]); }, onShowActionsOnEntityClick: (node) => { notifications?.toasts.addInfo('Show actions on entity is not implemented yet'); @@ -114,6 +142,8 @@ const useGraphNodes = ( * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab */ export const GraphVisualization: React.FC = memo(() => { + const { indexPattern } = useSourcererDataView(SourcererScopeName.default); + const [searchFilters, setSearchFilters] = useState(() => []); const { filters, updateFilters } = useGraphFilters(); const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); const { eventIds } = useGraphPreview({ @@ -122,7 +152,7 @@ export const GraphVisualization: React.FC = memo(() => { }); const [actorIds, setActorIds] = useState(() => new Set()); - const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds); + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds, setSearchFilters); const expandButtonClickHandler = (...args: unknown[]) => popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); @@ -135,7 +165,7 @@ export const GraphVisualization: React.FC = memo(() => { {...{ appName: 'graph-visualization', intl: null, - showFilterBar: false, + showFilterBar: true, showDatePicker: true, showAutoRefreshOnly: false, showSaveQuery: false, @@ -145,12 +175,24 @@ export const GraphVisualization: React.FC = memo(() => { dateRangeFrom: filters.from.split('/')[0], dateRangeTo: filters.to.split('/')[0], query: { query: '', language: 'kuery' }, - filters: [], + indexPatterns: [indexPattern], + filters: searchFilters, submitButtonStyle: 'iconOnly', + onFiltersUpdated: (newFilters) => { + setSearchFilters(newFilters); + + setActorIds( + new Set( + newFilters + .filter((filter) => filter.meta.key === 'actor.entity.id') + .map((filter) => (filter.meta.params as { query: string })?.query) + .filter((query) => typeof query === 'string') + ) + ); + }, onQuerySubmit: (payload, isUpdate) => { if (isUpdate) { updateFilters({ from: payload.dateRange.from, to: payload.dateRange.to }); - setActorIds(new Set()); } else { refresh(); } From b88b0740a0735c988d4fe9323f668e78d3030663 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Thu, 31 Oct 2024 15:19:18 +0000 Subject: [PATCH 07/36] fixed prevent scrolling when not interactive --- .../graph/src/components/graph/graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index ded6cc868fd3d..b32bd9b1c718e 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -160,7 +160,7 @@ export const Graph: React.FC = ({ zoomOnScroll={!isGraphLocked && !isLockedProp} zoomOnPinch={!isGraphLocked && !isLockedProp} zoomOnDoubleClick={!isGraphLocked && !isLockedProp} - preventScrolling={!interactive} + preventScrolling={interactive} nodesDraggable={interactive && !isGraphLocked && !isLockedProp} maxZoom={1.3} > From dbb26884cd6fd41b178fe5b3e74c79f99af0e2ef Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 4 Nov 2024 16:58:17 +0000 Subject: [PATCH 08/36] Added filter support --- .../common/schema/graph/v1.ts | 9 +- .../server/routes/graph/route.ts | 5 +- .../server/routes/graph/v1.ts | 86 +++++++------ .../left/components/graph_visualization.tsx | 119 +++++++++++++----- .../right/hooks/use_fetch_graph_data.ts | 4 +- 5 files changed, 146 insertions(+), 77 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts index 3d37331b4cc5d..d8c7f0cfcbc7c 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -9,11 +9,18 @@ import { schema } from '@kbn/config-schema'; export const graphRequestSchema = schema.object({ query: schema.object({ - actorIds: schema.arrayOf(schema.string()), eventIds: schema.arrayOf(schema.string()), // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), + esQuery: schema.object({ + bool: schema.object({ + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must_not: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + }), + }), }), }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts index 9ff15c2be73e6..7edcb876d9c85 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -10,6 +10,7 @@ import { graphResponseSchema, } from '@kbn/cloud-security-posture-common/schema/graph/latest'; import { transformError } from '@kbn/securitysolution-es-utils'; +import { BoolQuery } from '@kbn/es-query'; import { GRAPH_ROUTE_PATH } from '../../../common/constants'; import { CspRouter } from '../../types'; import { getGraph as getGraphV1 } from './v1'; @@ -37,7 +38,7 @@ export const defineGraphRoute = (router: CspRouter) => }, }, async (context, request, response) => { - const { actorIds, eventIds, start, end } = request.body.query; + const { eventIds, start, end, esQuery } = request.body.query; const cspContext = await context.csp; const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id; @@ -48,7 +49,7 @@ export const defineGraphRoute = (router: CspRouter) => esClient: cspContext.esClient, }, { - actorIds, + esQuery: esQuery as { bool: BoolQuery }, eventIds, spaceId, start, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index 5102d153c1905..2614d12bf7454 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -17,6 +17,7 @@ import type { } from '@kbn/cloud-security-posture-common/types/graph/latest'; import type { EsqlToRecords } from '@elastic/elasticsearch/lib/helpers'; import type { Writable } from '@kbn/utility-types'; +import { BoolQuery } from '@kbn/es-query'; import type { GraphContextServices, GraphContext } from './types'; interface GraphEdge { @@ -39,7 +40,7 @@ interface LabelEdges { export const getGraph = async ( services: GraphContextServices, query: { - actorIds: string[]; + esQuery: { bool: BoolQuery }; eventIds: string[]; spaceId?: string; start: string | number; @@ -50,15 +51,11 @@ export const getGraph = async ( edges: EdgeDataModel[]; }> => { const { esClient, logger } = services; - const { actorIds, eventIds, spaceId = 'default', start, end } = query; + const { esQuery, eventIds, spaceId = 'default', start, end } = query; - logger.trace( - `Fetching graph for [eventIds: ${eventIds.join(', ')}] [actorIds: ${actorIds.join( - ', ' - )}] in [spaceId: ${spaceId}]` - ); + logger.trace(`Fetching graph for [eventIds: ${eventIds.join(', ')}] in [spaceId: ${spaceId}]`); - const results = await fetchGraph({ esClient, logger, start, end, eventIds, actorIds }); + const results = await fetchGraph({ esClient, logger, start, end, eventIds, esQuery }); // Convert results into set of nodes and edges const graphContext = parseRecords(logger, results.records); @@ -98,14 +95,14 @@ const fetchGraph = async ({ logger, start, end, - actorIds, + esQuery, eventIds, }: { esClient: IScopedClusterClient; logger: Logger; start: string | number; end: string | number; - actorIds: string[]; + esQuery: { bool: BoolQuery }; eventIds: string[]; }): Promise> => { const query = `from logs-* @@ -127,47 +124,56 @@ const fetchGraph = async ({ | LIMIT 1000`; logger.trace(`Executing query [${query}]`); + logger.trace(JSON.stringify(buildDslFilter(eventIds, start, end, esQuery))); return await esClient.asCurrentUser.helpers .esql({ columnar: false, - filter: { + filter: buildDslFilter(eventIds, start, end, esQuery), + query, + // @ts-ignore - types are not up to date + params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], + }) + .toRecords(); +}; + +const buildDslFilter = ( + eventIds: string[], + start: string | number, + end: string | number, + esQuery: { bool: BoolQuery } +) => ({ + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: start, + lte: end, + }, + }, + }, + { bool: { - must: [ + should: [ + ...(esQuery.bool.filter.length || + esQuery.bool.must.length || + esQuery.bool.should.length || + esQuery.bool.must_not.length + ? [esQuery] + : []), { - range: { - '@timestamp': { - gte: start, - lte: end, - }, - }, - }, - { - bool: { - should: [ - { - terms: { - 'event.id': eventIds, - }, - }, - { - terms: { - 'actor.entity.id': actorIds, - }, - }, - ], - minimum_should_match: 1, + terms: { + 'event.id': eventIds, }, }, ], + minimum_should_match: 1, }, }, - query, - // @ts-ignore - types are not up to date - params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], - }) - .toRecords(); -}; + ], + }, +}); const createNodes = ( logger: Logger, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index fed990c3572ad..c3cd28575043f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import React, { memo, useMemo, useState } from 'react'; +import React, { memo, useEffect, useMemo, useState } from 'react'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { NodeViewModel } from '@kbn/cloud-security-posture-graph'; -import type { Filter, Query } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; +import type { Filter, Query, TimeRange, BoolQuery } from '@kbn/es-query'; import { css } from '@emotion/css'; +import { getEsQueryConfig } from '@kbn/data-service'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; @@ -27,31 +29,27 @@ const GraphLazy = React.lazy(() => import('@kbn/cloud-security-posture-graph').then((module) => ({ default: module.Graph })) ); -const useGraphFilters = () => { - const [filters, setFilters] = useState({ +const useTimeRange = () => { + const [timeRange, setTimeRange] = useState({ from: DEFAULT_FROM, to: DEFAULT_TO, }); - const updateFilters = (newFilters: Partial) => { - setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); + const setPartialTimeRange = (newTimeRange: Partial) => { + setTimeRange((currTimeRange) => ({ ...currTimeRange, ...newTimeRange })); }; - return { filters, updateFilters }; + return { timeRange, setTimeRange, setPartialTimeRange }; }; -const useGraphData = ( - actorIds: Set, - eventIds: string[], - filters: { from: string; to: string } -) => { +const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: BoolQuery }) => { const { data, refresh, isFetching } = useFetchGraphData({ req: { query: { - actorIds: Array.from(actorIds), eventIds, - start: filters.from, - end: filters.to, + esQuery: filter, + start: timeRange.from, + end: timeRange.to, }, }, options: { @@ -73,7 +71,29 @@ const useGraphPopovers = ( const nodeExpandPopover = useGraphNodeExpandPopover({ onExploreRelatedEntitiesClick: (node) => { - notifications?.toasts.addInfo('Explore related entities is not implemented yet'); + setSearchFilters((prev) => [ + ...prev, + { + meta: { + disabled: false, + key: 'related.entity', + field: 'related.entity', + negate: false, + params: { + query: node.id, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'related.entity': node.id, + }, + }, + $state: { + store: 'appState', + } as Filter['$state'], + }, + ]); }, onShowActionsByEntityClick: (node) => { setActorIds((prev) => new Set([...prev, node.id])); @@ -102,7 +122,29 @@ const useGraphPopovers = ( ]); }, onShowActionsOnEntityClick: (node) => { - notifications?.toasts.addInfo('Show actions on entity is not implemented yet'); + setSearchFilters((prev) => [ + ...prev, + { + meta: { + disabled: false, + key: 'target.entity.id', + field: 'target.entity.id', + negate: false, + params: { + query: node.id, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'target.entity.id': node.id, + }, + }, + $state: { + store: 'appState', + } as Filter['$state'], + }, + ]); }, onViewEntityDetailsClick: (node) => { notifications?.toasts.addInfo('View entity details is not implemented yet'); @@ -144,19 +186,41 @@ const useGraphNodes = ( export const GraphVisualization: React.FC = memo(() => { const { indexPattern } = useSourcererDataView(SourcererScopeName.default); const [searchFilters, setSearchFilters] = useState(() => []); - const { filters, updateFilters } = useGraphFilters(); + const { timeRange, setTimeRange } = useTimeRange(); const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); const { eventIds } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); - const [actorIds, setActorIds] = useState(() => new Set()); + const { + services: { uiSettings }, + } = useKibana(); + const [query, setQuery] = useState<{ bool: BoolQuery }>( + buildEsQuery( + indexPattern, + [], + [...searchFilters], + getEsQueryConfig(uiSettings as Parameters[0]) + ) + ); + + useEffect(() => { + setQuery( + buildEsQuery( + indexPattern, + [], + [...searchFilters], + getEsQueryConfig(uiSettings as Parameters[0]) + ) + ); + }, [searchFilters, indexPattern, uiSettings]); + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds, setSearchFilters); const expandButtonClickHandler = (...args: unknown[]) => popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); - const { data, refresh, isFetching } = useGraphData(actorIds, eventIds, filters); + const { data, refresh, isFetching } = useGraphData(eventIds, timeRange, query); const nodes = useGraphNodes(data?.nodes ?? [], expandButtonClickHandler); return ( @@ -172,27 +236,18 @@ export const GraphVisualization: React.FC = memo(() => { showQueryInput: false, isLoading: isFetching, isAutoRefreshDisabled: true, - dateRangeFrom: filters.from.split('/')[0], - dateRangeTo: filters.to.split('/')[0], + dateRangeFrom: timeRange.from.split('/')[0], + dateRangeTo: timeRange.to.split('/')[0], query: { query: '', language: 'kuery' }, indexPatterns: [indexPattern], filters: searchFilters, submitButtonStyle: 'iconOnly', onFiltersUpdated: (newFilters) => { setSearchFilters(newFilters); - - setActorIds( - new Set( - newFilters - .filter((filter) => filter.meta.key === 'actor.entity.id') - .map((filter) => (filter.meta.params as { query: string })?.query) - .filter((query) => typeof query === 'string') - ) - ); }, onQuerySubmit: (payload, isUpdate) => { if (isUpdate) { - updateFilters({ from: payload.dateRange.from, to: payload.dateRange.to }); + setTimeRange({ from: payload.dateRange.from, to: payload.dateRange.to }); } else { refresh(); } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts index ada59030eb25e..7f231a26dea73 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts @@ -80,9 +80,9 @@ export const useFetchGraphData = ({ options, }: UseFetchGraphDataParams): UseFetchGraphDataResult => { const queryClient = useQueryClient(); - const { actorIds, eventIds, start, end } = req.query; + const { esQuery, eventIds, start, end } = req.query; const http = useHttp(); - const QUERY_KEY = ['useFetchGraphData', actorIds, eventIds, start, end]; + const QUERY_KEY = ['useFetchGraphData', eventIds, start, end, esQuery]; const { isLoading, isError, data, isFetching } = useQuery( QUERY_KEY, From 008521faf8aecb71c6131da8d12e1eebc041c219 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 4 Nov 2024 16:59:27 +0000 Subject: [PATCH 09/36] lowered the minimum zoom --- .../graph/src/components/graph/graph.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index b32bd9b1c718e..c3a36095b205b 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -163,6 +163,7 @@ export const Graph: React.FC = ({ preventScrolling={interactive} nodesDraggable={interactive && !isGraphLocked && !isLockedProp} maxZoom={1.3} + minZoom={0.1} > {interactive && } From 70ee31da09f9c39c0edbfb339ac4013876d3151d Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 4 Nov 2024 16:59:46 +0000 Subject: [PATCH 10/36] refactoring --- .../components/node/node_expand_button.tsx | 2 +- .../use_graph_node_expand_popover.tsx | 28 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx index ef86a5f7f452b..522b7d1b5d45b 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx @@ -23,7 +23,7 @@ export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { }, []); const onClickHandler = (e: React.MouseEvent) => { - setIsToggled(!isToggled); + setIsToggled((currIsToggled) => !currIsToggled); onClick?.(e, unToggleCallback); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx index 7ae2d331f3343..164111682a1f5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx @@ -39,30 +39,31 @@ export const useGraphNodeExpandPopover = (args: UseGraphNodeExpandPopoverArgs = unToggleCallback: () => void; } | null>(null); + const closePopoverHandler = useCallback(() => { + selectedNode.current = null; + unToggleCallbackRef.current?.(); + unToggleCallbackRef.current = null; + closePopover(); + }, [closePopover]); + const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( (e, node, unToggleCallback) => { if (selectedNode.current?.id === node.id) { // If the same node is clicked again, close the popover - selectedNode.current = null; - unToggleCallbackRef.current?.(); - unToggleCallbackRef.current = null; - closePopover(); + closePopoverHandler(); } else { // Close the current popover if open - selectedNode.current = null; - unToggleCallbackRef.current?.(); - unToggleCallbackRef.current = null; + closePopoverHandler(); // Set the pending open state setPendingOpen({ node, el: e.currentTarget, unToggleCallback }); - - closePopover(); } }, - [closePopover] + [closePopoverHandler] ); useEffect(() => { + // Open pending popover if the popover is not open if (!state.isOpen && pendingOpen) { const { node, el, unToggleCallback } = pendingOpen; @@ -74,13 +75,6 @@ export const useGraphNodeExpandPopover = (args: UseGraphNodeExpandPopoverArgs = } }, [state.isOpen, pendingOpen, openPopover]); - const closePopoverHandler = useCallback(() => { - selectedNode.current = null; - unToggleCallbackRef.current?.(); - unToggleCallbackRef.current = null; - closePopover(); - }, [closePopover]); - const PopoverComponent = memo(() => ( Date: Mon, 4 Nov 2024 20:53:23 +0000 Subject: [PATCH 11/36] Added open in timeline + time picking is around @timestamp --- .../common/schema/graph/v1.ts | 18 +- .../server/routes/graph/v1.ts | 14 +- .../left/components/graph_visualization.tsx | 213 +++++++++++------- .../components/graph_preview_container.tsx | 8 +- 4 files changed, 154 insertions(+), 99 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts index d8c7f0cfcbc7c..0a7c38ddc4c11 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -13,14 +13,16 @@ export const graphRequestSchema = schema.object({ // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), - esQuery: schema.object({ - bool: schema.object({ - filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - must_not: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - }), - }), + esQuery: schema.maybe( + schema.object({ + bool: schema.object({ + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must_not: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + }), + }) + ), }), }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index 2614d12bf7454..7cb2d14f7f6e9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -40,7 +40,7 @@ interface LabelEdges { export const getGraph = async ( services: GraphContextServices, query: { - esQuery: { bool: BoolQuery }; + esQuery?: { bool: BoolQuery }; eventIds: string[]; spaceId?: string; start: string | number; @@ -102,7 +102,7 @@ const fetchGraph = async ({ logger: Logger; start: string | number; end: string | number; - esQuery: { bool: BoolQuery }; + esQuery?: { bool: BoolQuery }; eventIds: string[]; }): Promise> => { const query = `from logs-* @@ -141,7 +141,7 @@ const buildDslFilter = ( eventIds: string[], start: string | number, end: string | number, - esQuery: { bool: BoolQuery } + esQuery?: { bool: BoolQuery } ) => ({ bool: { filter: [ @@ -156,10 +156,10 @@ const buildDslFilter = ( { bool: { should: [ - ...(esQuery.bool.filter.length || - esQuery.bool.must.length || - esQuery.bool.should.length || - esQuery.bool.must_not.length + ...(esQuery?.bool.filter.length || + esQuery?.bool.must.length || + esQuery?.bool.should.length || + esQuery?.bool.must_not.length ? [esQuery] : []), { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index c3cd28575043f..54025e2ec4aae 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -13,6 +13,13 @@ import { buildEsQuery } from '@kbn/es-query'; import type { Filter, Query, TimeRange, BoolQuery } from '@kbn/es-query'; import { css } from '@emotion/css'; import { getEsQueryConfig } from '@kbn/data-service'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { DataProvider } from '@kbn/timelines-plugin/common/types'; +import { flatten } from 'lodash'; +import moment from 'moment'; +import { normalizeTimeRange } from '../../../../common/utils/normalize_time_range'; +import { getDataProvider } from '../../../../app/actions/add_to_timeline/data_provider'; +import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; @@ -29,10 +36,10 @@ const GraphLazy = React.lazy(() => import('@kbn/cloud-security-posture-graph').then((module) => ({ default: module.Graph })) ); -const useTimeRange = () => { +const useTimeRange = (timestamp: string) => { const [timeRange, setTimeRange] = useState({ - from: DEFAULT_FROM, - to: DEFAULT_TO, + from: moment(timestamp).subtract(30, 'minutes').toISOString(), + to: moment(timestamp).add(30, 'minutes').toISOString(), }); const setPartialTimeRange = (newTimeRange: Partial) => { @@ -61,90 +68,90 @@ const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: return { data, refresh, isFetching }; }; -const useGraphPopovers = ( - setActorIds: React.Dispatch>>, - setSearchFilters: React.Dispatch> -) => { +const addFilter = (prev: Filter[], key: string, value: string) => { + const [firstFilter, ...otherFilters] = prev; + + if (firstFilter?.meta?.type === 'combined' && firstFilter?.meta?.relation === 'OR') { + return [ + { + ...firstFilter, + meta: { + ...firstFilter.meta, + params: [ + ...(Array.isArray(firstFilter.meta.params) ? firstFilter.meta.params : []), + { + meta: { + disabled: false, + negate: false, + key, + field: key, + type: 'phrase', + params: { + query: value, + }, + }, + query: { + match_phrase: { + [key]: value, + }, + }, + }, + ], + }, + }, + ...otherFilters, + ]; + } else { + return [ + { + meta: { + params: [ + { + meta: { + disabled: false, + negate: false, + key, + field: key, + type: 'phrase', + params: { + query: value, + }, + }, + query: { + match_phrase: { + [key]: value, + }, + }, + }, + ], + type: 'combined', + relation: 'OR', + disabled: false, + negate: false, + }, + $state: { + store: 'appState', + } as Filter['$state'], + }, + ...prev, + ]; + } +}; + +const useGraphPopovers = (setSearchFilters: React.Dispatch>) => { const { services: { notifications }, } = useKibana(); const nodeExpandPopover = useGraphNodeExpandPopover({ onExploreRelatedEntitiesClick: (node) => { - setSearchFilters((prev) => [ - ...prev, - { - meta: { - disabled: false, - key: 'related.entity', - field: 'related.entity', - negate: false, - params: { - query: node.id, - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'related.entity': node.id, - }, - }, - $state: { - store: 'appState', - } as Filter['$state'], - }, - ]); + setSearchFilters((prev) => addFilter(prev, 'related.entity', node.id)); }, onShowActionsByEntityClick: (node) => { - setActorIds((prev) => new Set([...prev, node.id])); - setSearchFilters((prev) => [ - ...prev, - { - meta: { - disabled: false, - key: 'actor.entity.id', - field: 'actor.entity.id', - negate: false, - params: { - query: node.id, - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'actor.entity.id': node.id, - }, - }, - $state: { - store: 'appState', - } as Filter['$state'], - }, - ]); + setSearchFilters((prev) => addFilter(prev, 'actor.entity.id', node.id)); }, onShowActionsOnEntityClick: (node) => { - setSearchFilters((prev) => [ - ...prev, - { - meta: { - disabled: false, - key: 'target.entity.id', - field: 'target.entity.id', - negate: false, - params: { - query: node.id, - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'target.entity.id': node.id, - }, - }, - $state: { - store: 'appState', - } as Filter['$state'], - }, - ]); + setSearchFilters((prev) => addFilter(prev, 'target.entity.id', node.id)); }, onViewEntityDetailsClick: (node) => { notifications?.toasts.addInfo('View entity details is not implemented yet'); @@ -180,19 +187,51 @@ const useGraphNodes = ( }, [nodes]); }; +const convertSearchFiltersToDataProviders = (searchFilters: Filter[]): DataProvider[] => { + return flatten( + searchFilters.map((filter) => { + if ( + filter.meta.type === 'combined' && + filter.meta.relation === 'OR' && + Array.isArray(filter.meta.params) + ) { + return ( + filter.meta.params?.map((param) => + getDataProvider({ + field: param.meta.field, + id: param.meta.key, + value: param.meta.params.query, + }) + ) ?? [] + ); + } else { + return getDataProvider({ + field: filter.meta.key ?? '', + id: filter.meta.key ?? '', + value: filter.meta.params?.query ?? '', + }); + } + }) + ); +}; + /** * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab */ export const GraphVisualization: React.FC = memo(() => { const { indexPattern } = useSourcererDataView(SourcererScopeName.default); const [searchFilters, setSearchFilters] = useState(() => []); - const { timeRange, setTimeRange } = useTimeRange(); const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); const { eventIds } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); + const { timeRange, setTimeRange } = useTimeRange(getFieldsData('@timestamp')[0]); + const [dataProviders, setDataProviders] = useState( + eventIds.map((id) => getDataProvider({ field: 'event.id', id, value: id })) + ); + const { services: { uiSettings }, } = useKibana(); @@ -216,7 +255,7 @@ export const GraphVisualization: React.FC = memo(() => { ); }, [searchFilters, indexPattern, uiSettings]); - const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds, setSearchFilters); + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setSearchFilters); const expandButtonClickHandler = (...args: unknown[]) => popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); @@ -247,17 +286,29 @@ export const GraphVisualization: React.FC = memo(() => { }, onQuerySubmit: (payload, isUpdate) => { if (isUpdate) { - setTimeRange({ from: payload.dateRange.from, to: payload.dateRange.to }); + setTimeRange({ ...payload.dateRange }); } else { refresh(); } }, }} /> + + + { ecsData: dataAsNestedObject, }); + const timestamp = getFieldsData('@timestamp')[0]; + // TODO: default start and end might not capture the original event const { isLoading, isError, data } = useFetchGraphData({ req: { query: { - actorIds: [], eventIds, - start: DEFAULT_FROM, - end: DEFAULT_TO, + start: moment(timestamp).subtract(30, 'minutes').toISOString(), + end: moment(timestamp).add(30, 'minutes').toISOString(), }, }, options: { From 5318512293d97155f4510df22c627815843a988a Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 5 Nov 2024 13:07:18 +0000 Subject: [PATCH 12/36] Color fixes and auto fitview --- .../graph/src/components/graph/graph.tsx | 2 +- .../server/routes/graph/v1.ts | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index c3a36095b205b..f53a1d8282ca8 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -110,7 +110,7 @@ export const Graph: React.FC = ({ setPrevEdges(edges); setTimeout(() => { fitViewRef.current?.(); - }, 0); + }, 30); } // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when nodes or edges change }, [nodes, edges, setNodes, setEdges]); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index 7cb2d14f7f6e9..cf0da55e1c4b4 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -14,6 +14,7 @@ import type { EntityNodeDataModel, LabelNodeDataModel, GroupNodeDataModel, + Color, } from '@kbn/cloud-security-posture-common/types/graph/latest'; import type { EsqlToRecords } from '@elastic/elasticsearch/lib/helpers'; import type { Writable } from '@kbn/utility-types'; @@ -121,7 +122,8 @@ const fetchGraph = async ({ targetIds = target.entity.id, eventOutcome = event.outcome, isAlert -| LIMIT 1000`; +| LIMIT 1000 +| SORT isAlert desc`; logger.trace(`Executing query [${query}]`); logger.trace(JSON.stringify(buildDslFilter(eventIds, start, end, esQuery))); @@ -307,14 +309,7 @@ const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { }; nodesMap[groupNode.id] = groupNode; - connectEntitiesAndLabelNode( - logger, - edgesMap, - nodesMap, - labelEdges[edgeLabelsIds[0]].source, - groupNode.id, - labelEdges[edgeLabelsIds[0]].target - ); + let groupEdgesColor: Color = 'primary'; edgeLabelsIds.forEach((edgeLabelId) => { (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; @@ -326,7 +321,26 @@ const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { edgeLabelId, groupNode.id ); + + if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { + groupEdgesColor = 'danger'; + } else if ( + (nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'warning' && + groupEdgesColor !== 'danger' + ) { + groupEdgesColor = 'warning'; + } }); + + connectEntitiesAndLabelNode( + logger, + edgesMap, + nodesMap, + labelEdges[edgeLabelsIds[0]].source, + groupNode.id, + labelEdges[edgeLabelsIds[0]].target, + groupEdgesColor + ); } }); }; @@ -337,11 +351,12 @@ const connectEntitiesAndLabelNode = ( nodesMap: Record, sourceNodeId: string, labelNodeId: string, - targetNodeId: string + targetNodeId: string, + colorOverride?: Color ) => { [ - connectNodes(nodesMap, sourceNodeId, labelNodeId), - connectNodes(nodesMap, labelNodeId, targetNodeId), + connectNodes(nodesMap, sourceNodeId, labelNodeId, colorOverride), + connectNodes(nodesMap, labelNodeId, targetNodeId, colorOverride), ].forEach((edge) => { logger.trace(`Connecting nodes [${edge.source} -> ${edge.target}]`); edgesMap[edge.id] = edge; @@ -351,7 +366,8 @@ const connectEntitiesAndLabelNode = ( const connectNodes = ( nodesMap: Record, sourceNodeId: string, - targetNodeId: string + targetNodeId: string, + colorOverride?: Color ): EdgeDataModel => { const sourceNode = nodesMap[sourceNodeId]; const targetNode = nodesMap[targetNodeId]; @@ -366,6 +382,6 @@ const connectNodes = ( id: `a(${sourceNodeId})-b(${targetNodeId})`, source: sourceNodeId, target: targetNodeId, - color, + color: colorOverride ?? color, }; }; From 2d0d360090564b95e572932ddbd2170e105c9201 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:26:09 +0000 Subject: [PATCH 13/36] Removed trace message that might contain sensitive info --- x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index f3ae2e5a0cc8c..2bd2e198d295f 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -164,7 +164,6 @@ const fetchGraph = async ({ | SORT isAlert DESC`; logger.trace(`Executing query [${query}]`); - logger.trace(JSON.stringify(buildDslFilter(eventIds, start, end, esQuery))); return await esClient.asCurrentUser.helpers .esql({ From f94db1e1439308611ada08134e589af8775d4c7a Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:27:56 +0000 Subject: [PATCH 14/36] Removed duplicated code due to merge --- .../server/routes/graph/v1.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index 2bd2e198d295f..b14a2ba3e06a9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -372,28 +372,6 @@ const createEdgesAndGroups = (context: ParseContext) => { } }); - let groupEdgesColor: Color = 'primary'; - - edgeLabelsIds.forEach((edgeLabelId) => { - (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; - connectEntitiesAndLabelNode( - edgesMap, - nodesMap, - groupNode.id, - edgeLabelId, - groupNode.id - ); - - if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { - groupEdgesColor = 'danger'; - } else if ( - (nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'warning' && - groupEdgesColor !== 'danger' - ) { - groupEdgesColor = 'warning'; - } - }); - connectEntitiesAndLabelNode( edgesMap, nodesMap, From 702ef002ac582492cf98956ffd374ef7cb75a93a Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Wed, 13 Nov 2024 00:14:34 +0000 Subject: [PATCH 15/36] merge cleanup --- .../src/components/graph_popovers.stories.tsx | 218 ------------------ .../src/components/node/diamond_node.tsx | 2 +- .../src/components/node/ellipse_node.tsx | 3 +- .../src/components/node/hexagon_node.tsx | 3 +- .../src/components/node/pentagon_node.tsx | 2 +- .../src/components/node/rectangle_node.tsx | 2 +- 6 files changed, 5 insertions(+), 225 deletions(-) delete mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx deleted file mode 100644 index 87bdea248ed5c..0000000000000 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_popovers.stories.tsx +++ /dev/null @@ -1,218 +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, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { ThemeProvider } from '@emotion/react'; -import { Story } from '@storybook/react'; -import { css } from '@emotion/react'; -import { EuiListGroup, EuiHorizontalRule, useEuiTheme } from '@elastic/eui'; -import type { EntityNodeViewModel, NodeProps } from '.'; -import { Graph } from '.'; -import { GraphPopover } from './graph/graph_popover'; -import { ExpandButtonClickCallback } from './types'; -import { useGraphPopover } from './graph/use_graph_popover'; -import { ExpandPopoverListItem } from './styles'; - -export default { - title: 'Components/Graph Components/Graph Popovers', - description: 'CDR - Graph visualization', - argTypes: {}, -}; - -const useExpandButtonPopover = () => { - const { id, state, actions } = useGraphPopover('node-expand-popover'); - const { openPopover, closePopover } = actions; - - const selectedNode = useRef(null); - const unToggleCallbackRef = useRef<(() => void) | null>(null); - const [pendingOpen, setPendingOpen] = useState<{ - node: NodeProps; - el: HTMLElement; - unToggleCallback: () => void; - } | null>(null); - - const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( - (e, node, unToggleCallback) => { - if (selectedNode.current?.id === node.id) { - // If the same node is clicked again, close the popover - selectedNode.current = null; - unToggleCallbackRef.current?.(); - unToggleCallbackRef.current = null; - closePopover(); - } else { - // Close the current popover if open - selectedNode.current = null; - unToggleCallbackRef.current?.(); - unToggleCallbackRef.current = null; - - // Set the pending open state - setPendingOpen({ node, el: e.currentTarget, unToggleCallback }); - - closePopover(); - } - }, - [closePopover] - ); - - useEffect(() => { - if (!state.isOpen && pendingOpen) { - const { node, el, unToggleCallback } = pendingOpen; - - selectedNode.current = node; - unToggleCallbackRef.current = unToggleCallback; - openPopover(el); - - setPendingOpen(null); - } - }, [state.isOpen, pendingOpen, openPopover]); - - const closePopoverHandler = useCallback(() => { - selectedNode.current = null; - unToggleCallbackRef.current?.(); - unToggleCallbackRef.current = null; - closePopover(); - }, [closePopover]); - - const { euiTheme } = useEuiTheme(); - - const PopoverComponent = memo(() => ( - - - {}} - /> - {}} - /> - {}} - /> - - {}} /> - - - )); - - const actionsWithClose = useMemo( - () => ({ - ...actions, - closePopover: closePopoverHandler, - }), - [actions, closePopoverHandler] - ); - - return useMemo( - () => ({ - onNodeExpandButtonClick, - Popover: PopoverComponent, - id, - actions: actionsWithClose, - state, - }), - [PopoverComponent, actionsWithClose, id, onNodeExpandButtonClick, state] - ); -}; - -const useNodePopover = () => { - const { id, state, actions } = useGraphPopover('node-popover'); - - const PopoverComponent = memo(() => ( - - TODO - - )); - - return useMemo( - () => ({ - onNodeClick: (e: React.MouseEvent) => actions.openPopover(e.currentTarget), - Popover: PopoverComponent, - id, - actions, - state, - }), - [PopoverComponent, actions, id, state] - ); -}; - -const Template: Story = () => { - const expandNodePopover = useExpandButtonPopover(); - const nodePopover = useNodePopover(); - const popovers = [expandNodePopover, nodePopover]; - const isPopoverOpen = popovers.some((popover) => popover.state.isOpen); - - const popoverOpenWrapper = (cb: Function, ...args: any[]) => { - [expandNodePopover.actions.closePopover, nodePopover.actions.closePopover].forEach( - (closePopover) => { - closePopover(); - } - ); - cb.apply(null, args); - }; - - const expandButtonClickHandler = (...args: any[]) => - popoverOpenWrapper(expandNodePopover.onNodeExpandButtonClick, ...args); - const nodeClickHandler = (...args: any[]) => popoverOpenWrapper(nodePopover.onNodeClick, ...args); - - const nodes: EntityNodeViewModel[] = useMemo( - () => - (['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const).map((shape, idx) => ({ - id: `${idx}`, - label: `Node ${idx}`, - color: 'primary', - icon: 'okta', - interactive: true, - shape, - expandButtonClick: expandButtonClickHandler, - nodeClick: nodeClickHandler, - })), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - return ( - - - {popovers?.map((popover) => popover.Popover && )} - - ); -}; - -export const GraphPopovers = Template.bind({}); diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx index 413f824ea3eb7..75ad989b625e8 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx @@ -15,8 +15,8 @@ import { NodeShapeOnHoverSvg, NodeShapeSvg, NodeIcon, - HandleStyleOverride, NodeButton, + HandleStyleOverride, } from './styles'; import { DiamondHoverShape, DiamondShape } from './shapes/diamond_shape'; import { NodeExpandButton } from './node_expand_button'; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx index 63bb5ba6515a2..c9bd363130dca 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx @@ -14,8 +14,8 @@ import { NodeShapeOnHoverSvg, NodeShapeSvg, NodeIcon, - HandleStyleOverride, NodeButton, + HandleStyleOverride, } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { EllipseHoverShape, EllipseShape } from './shapes/ellipse_shape'; @@ -48,7 +48,6 @@ export const EllipseNode: React.FC = memo((props: NodeProps) => { viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg" - className="node-shape" > = memo((props: NodeProps) => { viewBox="0 0 71 78" fill="none" xmlns="http://www.w3.org/2000/svg" - className="node-shape" > Date: Mon, 18 Nov 2024 16:27:37 +0000 Subject: [PATCH 16/36] refactoring, removed investigate in timeline --- .../graph/src/components/styles.tsx | 23 +- .../components/graph_node_expand_popover.tsx | 96 +++---- .../left/components/graph_visualization.tsx | 247 +++++++----------- .../left/components/test_ids.ts | 6 + .../use_graph_node_expand_popover.tsx | 29 +- .../components/graph_preview_container.tsx | 2 - .../right/hooks/use_fetch_graph_data.ts | 4 +- .../right/hooks/use_graph_preview.test.tsx | 97 ++++--- .../right/hooks/use_graph_preview.ts | 14 +- .../es_archives/logs_gcp_audit/data.json | 25 ++ 10 files changed, 262 insertions(+), 281 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx index 0efff1c88456c..be776d57be12a 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx @@ -6,13 +6,16 @@ */ import React from 'react'; +import type { + EuiIconProps, + _EuiBackgroundColor, + CommonProps, + EuiListGroupItemProps, +} from '@elastic/eui'; import { - EuiIcon, useEuiBackgroundColor, useEuiTheme, - type EuiIconProps, - type _EuiBackgroundColor, - EuiListGroupItemProps, + EuiIcon, EuiListGroupItem, EuiText, } from '@elastic/eui'; @@ -59,22 +62,24 @@ const RoundedEuiIcon: React.FC = ({ color, background, ...r ); export const ExpandPopoverListItem: React.FC< - Pick + CommonProps & Pick > = (props) => { + const { iconType, label, onClick, ...rest } = props; const { euiTheme } = useEuiTheme(); return ( + iconType ? ( + ) : undefined } label={ - {props.label} + {label} } - onClick={props.onClick} + onClick={onClick} /> ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx index 93450b644f721..fd22315f3562e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx @@ -7,19 +7,22 @@ import React, { memo } from 'react'; import { GraphPopover } from '@kbn/cloud-security-posture-graph'; -import { EuiHorizontalRule, EuiListGroup, useEuiTheme } from '@elastic/eui'; +import { EuiListGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ExpandPopoverListItem } from '@kbn/cloud-security-posture-graph/src/components/styles'; -import { css } from '@emotion/react'; +import { + GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID, + GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID, + GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID, +} from './test_ids'; interface GraphNodeExpandPopoverProps { isOpen: boolean; anchorElement: HTMLElement | null; closePopover: () => void; - onExploreRelatedEntitiesClick?: () => void; - onShowActionsByEntityClick?: () => void; - onShowActionsOnEntityClick?: () => void; - onViewEntityDetailsClick?: () => void; + onExploreRelatedEntitiesClick: () => void; + onShowActionsByEntityClick: () => void; + onShowActionsOnEntityClick: () => void; } export const GraphNodeExpandPopover: React.FC = memo( @@ -30,70 +33,43 @@ export const GraphNodeExpandPopover: React.FC = mem onExploreRelatedEntitiesClick, onShowActionsByEntityClick, onShowActionsOnEntityClick, - onViewEntityDetailsClick, }) => { - const { euiTheme } = useEuiTheme(); - return ( - {onExploreRelatedEntitiesClick && ( - - )} - {onShowActionsByEntityClick && ( - - )} - {onShowActionsOnEntityClick && ( - - )} - {onViewEntityDetailsClick && ( - <> - - - - )} + + + ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index 54025e2ec4aae..94f8137d8a4ac 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -9,18 +9,17 @@ import React, { memo, useEffect, useMemo, useState } from 'react'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { NodeViewModel } from '@kbn/cloud-security-posture-graph'; -import { buildEsQuery } from '@kbn/es-query'; -import type { Filter, Query, TimeRange, BoolQuery } from '@kbn/es-query'; +import { + BooleanRelation, + buildEsQuery, + isCombinedFilter, + buildCombinedFilter, + isPhraseFilter, +} from '@kbn/es-query'; +import type { Filter, Query, TimeRange, BoolQuery, PhraseFilter } from '@kbn/es-query'; import { css } from '@emotion/css'; import { getEsQueryConfig } from '@kbn/data-service'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { DataProvider } from '@kbn/timelines-plugin/common/types'; -import { flatten } from 'lodash'; -import moment from 'moment'; -import { normalizeTimeRange } from '../../../../common/utils/normalize_time_range'; -import { getDataProvider } from '../../../../app/actions/add_to_timeline/data_provider'; -import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { useGetScopedSourcererDataView } from '../../../../sourcerer/components/use_get_sourcerer_data_view'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_VISUALIZATION_TEST_ID } from './test_ids'; @@ -29,8 +28,7 @@ import { useGraphPreview } from '../../right/hooks/use_graph_preview'; import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; export const GRAPH_VISUALIZATION_ID = 'graph_visualization'; -const DEFAULT_FROM = 'now-60d/d'; -const DEFAULT_TO = 'now/d'; +const CONTROLLED_BY_GRAPH_VISUALIZATION_FILTER = 'graph-visualization'; const GraphLazy = React.lazy(() => import('@kbn/cloud-security-posture-graph').then((module) => ({ default: module.Graph })) @@ -38,8 +36,8 @@ const GraphLazy = React.lazy(() => const useTimeRange = (timestamp: string) => { const [timeRange, setTimeRange] = useState({ - from: moment(timestamp).subtract(30, 'minutes').toISOString(), - to: moment(timestamp).add(30, 'minutes').toISOString(), + from: `${timestamp}||-30m`, + to: `${timestamp}||+30m`, }); const setPartialTimeRange = (newTimeRange: Partial) => { @@ -68,10 +66,29 @@ const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: return { data, refresh, isFetching }; }; -const addFilter = (prev: Filter[], key: string, value: string) => { +const buildPhraseFilter = (field: string, value: string): PhraseFilter => ({ + meta: { + key: field, + negate: false, + disabled: false, + type: 'phrase', + field, + controlledBy: CONTROLLED_BY_GRAPH_VISUALIZATION_FILTER, + params: { + query: value, + }, + }, + query: { + match_phrase: { + [field]: value, + }, + }, +}); + +const addFilter = (dataViewId: string, prev: Filter[], key: string, value: string) => { const [firstFilter, ...otherFilters] = prev; - if (firstFilter?.meta?.type === 'combined' && firstFilter?.meta?.relation === 'OR') { + if (isCombinedFilter(firstFilter) && firstFilter?.meta?.relation === BooleanRelation.OR) { return [ { ...firstFilter, @@ -79,82 +96,37 @@ const addFilter = (prev: Filter[], key: string, value: string) => { ...firstFilter.meta, params: [ ...(Array.isArray(firstFilter.meta.params) ? firstFilter.meta.params : []), - { - meta: { - disabled: false, - negate: false, - key, - field: key, - type: 'phrase', - params: { - query: value, - }, - }, - query: { - match_phrase: { - [key]: value, - }, - }, - }, + buildPhraseFilter(key, value), ], }, }, ...otherFilters, ]; - } else { + } else if (isPhraseFilter(firstFilter)) { return [ - { - meta: { - params: [ - { - meta: { - disabled: false, - negate: false, - key, - field: key, - type: 'phrase', - params: { - query: value, - }, - }, - query: { - match_phrase: { - [key]: value, - }, - }, - }, - ], - type: 'combined', - relation: 'OR', - disabled: false, - negate: false, - }, - $state: { - store: 'appState', - } as Filter['$state'], - }, - ...prev, + buildCombinedFilter(BooleanRelation.OR, [firstFilter, buildPhraseFilter(key, value)], { + id: dataViewId, + }), + ...otherFilters, ]; + } else { + return [buildPhraseFilter(key, value), ...prev]; } }; -const useGraphPopovers = (setSearchFilters: React.Dispatch>) => { - const { - services: { notifications }, - } = useKibana(); - +const useGraphPopovers = ( + dataViewId: string, + setSearchFilters: React.Dispatch> +) => { const nodeExpandPopover = useGraphNodeExpandPopover({ onExploreRelatedEntitiesClick: (node) => { - setSearchFilters((prev) => addFilter(prev, 'related.entity', node.id)); + setSearchFilters((prev) => addFilter(dataViewId, prev, 'related.entity', node.id)); }, onShowActionsByEntityClick: (node) => { - setSearchFilters((prev) => addFilter(prev, 'actor.entity.id', node.id)); + setSearchFilters((prev) => addFilter(dataViewId, prev, 'actor.entity.id', node.id)); }, onShowActionsOnEntityClick: (node) => { - setSearchFilters((prev) => addFilter(prev, 'target.entity.id', node.id)); - }, - onViewEntityDetailsClick: (node) => { - notifications?.toasts.addInfo('View entity details is not implemented yet'); + setSearchFilters((prev) => addFilter(dataViewId, prev, 'target.entity.id', node.id)); }, }); @@ -187,57 +159,28 @@ const useGraphNodes = ( }, [nodes]); }; -const convertSearchFiltersToDataProviders = (searchFilters: Filter[]): DataProvider[] => { - return flatten( - searchFilters.map((filter) => { - if ( - filter.meta.type === 'combined' && - filter.meta.relation === 'OR' && - Array.isArray(filter.meta.params) - ) { - return ( - filter.meta.params?.map((param) => - getDataProvider({ - field: param.meta.field, - id: param.meta.key, - value: param.meta.params.query, - }) - ) ?? [] - ); - } else { - return getDataProvider({ - field: filter.meta.key ?? '', - id: filter.meta.key ?? '', - value: filter.meta.params?.query ?? '', - }); - } - }) - ); -}; - /** * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab */ export const GraphVisualization: React.FC = memo(() => { - const { indexPattern } = useSourcererDataView(SourcererScopeName.default); + const dataView = useGetScopedSourcererDataView({ + sourcererScope: SourcererScopeName.default, + }); const [searchFilters, setSearchFilters] = useState(() => []); const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); - const { eventIds } = useGraphPreview({ + const { eventIds, timestamp } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); - const { timeRange, setTimeRange } = useTimeRange(getFieldsData('@timestamp')[0]); - const [dataProviders, setDataProviders] = useState( - eventIds.map((id) => getDataProvider({ field: 'event.id', id, value: id })) - ); + const { timeRange, setTimeRange } = useTimeRange(timestamp ?? new Date().toISOString()); const { services: { uiSettings }, } = useKibana(); const [query, setQuery] = useState<{ bool: BoolQuery }>( buildEsQuery( - indexPattern, + dataView, [], [...searchFilters], getEsQueryConfig(uiSettings as Parameters[0]) @@ -247,15 +190,18 @@ export const GraphVisualization: React.FC = memo(() => { useEffect(() => { setQuery( buildEsQuery( - indexPattern, + dataView, [], [...searchFilters], getEsQueryConfig(uiSettings as Parameters[0]) ) ); - }, [searchFilters, indexPattern, uiSettings]); + }, [searchFilters, dataView, uiSettings]); - const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setSearchFilters); + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers( + dataView?.id ?? '', + setSearchFilters + ); const expandButtonClickHandler = (...args: unknown[]) => popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); @@ -264,51 +210,42 @@ export const GraphVisualization: React.FC = memo(() => { return (
- - {...{ - appName: 'graph-visualization', - intl: null, - showFilterBar: true, - showDatePicker: true, - showAutoRefreshOnly: false, - showSaveQuery: false, - showQueryInput: false, - isLoading: isFetching, - isAutoRefreshDisabled: true, - dateRangeFrom: timeRange.from.split('/')[0], - dateRangeTo: timeRange.to.split('/')[0], - query: { query: '', language: 'kuery' }, - indexPatterns: [indexPattern], - filters: searchFilters, - submitButtonStyle: 'iconOnly', - onFiltersUpdated: (newFilters) => { - setSearchFilters(newFilters); - }, - onQuerySubmit: (payload, isUpdate) => { - if (isUpdate) { - setTimeRange({ ...payload.dateRange }); - } else { - refresh(); - } - }, - }} - /> - - + {...{ + appName: 'graph-visualization', + intl: null, + showFilterBar: true, + showDatePicker: true, + showAutoRefreshOnly: false, + showSaveQuery: false, + showQueryInput: false, + isLoading: isFetching, + isAutoRefreshDisabled: true, + dateRangeFrom: timeRange.from.split('/')[0], + dateRangeTo: timeRange.to.split('/')[0], + query: { query: '', language: 'kuery' }, + indexPatterns: [dataView], + filters: searchFilters, + submitButtonStyle: 'iconOnly', + onFiltersUpdated: (newFilters) => { + setSearchFilters(newFilters); + }, + onQuerySubmit: (payload, isUpdate) => { + if (isUpdate) { + setTimeRange({ ...payload.dateRange }); + } else { + refresh(); + } + }, + }} /> - + )} void; - onShowActionsByEntityClick?: (node: NodeProps) => void; - onShowActionsOnEntityClick?: (node: NodeProps) => void; - onViewEntityDetailsClick?: (node: NodeProps) => void; + onExploreRelatedEntitiesClick: (node: NodeProps) => void; + onShowActionsByEntityClick: (node: NodeProps) => void; + onShowActionsOnEntityClick: (node: NodeProps) => void; } -export const useGraphNodeExpandPopover = (args: UseGraphNodeExpandPopoverArgs = {}) => { - const { - onExploreRelatedEntitiesClick, - onShowActionsByEntityClick, - onShowActionsOnEntityClick, - onViewEntityDetailsClick, - } = args; +export const useGraphNodeExpandPopover = ({ + onExploreRelatedEntitiesClick, + onShowActionsByEntityClick, + onShowActionsOnEntityClick, +}: UseGraphNodeExpandPopoverArgs) => { const { id, state, actions } = useGraphPopover('node-expand-popover'); const { openPopover, closePopover } = actions; @@ -81,19 +78,15 @@ export const useGraphNodeExpandPopover = (args: UseGraphNodeExpandPopoverArgs = anchorElement={state.anchorElement} closePopover={closePopoverHandler} onExploreRelatedEntitiesClick={() => { - onExploreRelatedEntitiesClick?.(selectedNode.current as NodeProps); + onExploreRelatedEntitiesClick(selectedNode.current as NodeProps); closePopoverHandler(); }} onShowActionsByEntityClick={() => { - onShowActionsByEntityClick?.(selectedNode.current as NodeProps); + onShowActionsByEntityClick(selectedNode.current as NodeProps); closePopoverHandler(); }} onShowActionsOnEntityClick={() => { - onShowActionsOnEntityClick?.(selectedNode.current as NodeProps); - closePopoverHandler(); - }} - onViewEntityDetailsClick={() => { - onViewEntityDetailsClick?.(selectedNode.current as NodeProps); + onShowActionsOnEntityClick(selectedNode.current as NodeProps); closePopoverHandler(); }} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 30dd4a5b653c9..6a1533e280815 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -44,8 +44,6 @@ export const GraphPreviewContainer: React.FC = () => { ecsData: dataAsNestedObject, }); - const timestamp = getFieldsData('@timestamp')[0]; - // TODO: default start and end might not capture the original event const { isLoading, isError, data } = useFetchGraphData({ req: { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts index 705f92d4eac49..964f5824f9074 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts @@ -49,11 +49,11 @@ export interface UseFetchGraphDataParams { */ export interface UseFetchGraphDataResult { /** - * Indicates if the query is currently loading. + * Indicates if the query is currently being fetched for the first time. */ isLoading: boolean; /** - * Indicates if the query is currently fetching. + * Indicates if the query is currently being fetched. Regardless of whether it is the initial fetch or a refetch. */ isFetching: boolean; /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx index d12154a390abf..a69040be9c458 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx @@ -12,15 +12,27 @@ import { useGraphPreview } from './use_graph_preview'; import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { mockFieldData } from '../../shared/mocks/mock_get_fields_data'; +const mockGetFieldsData: GetFieldsData = (field: string) => { + if (field === 'kibana.alert.original_event.id') { + return 'eventId'; + } else if (field === 'actor.entity.id') { + return 'actorId'; + } else if (field === 'target.entity.id') { + return 'targetId'; + } + + return mockFieldData[field]; +}; + describe('useGraphPreview', () => { let hookResult: RenderHookResult; it(`should return false when missing actor`, () => { const getFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { - return 'eventId'; + if (field === 'actor.entity.id') { + return; } - return mockFieldData[field]; + return mockGetFieldsData(field); }; hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { @@ -35,22 +47,42 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; expect(isAuditLog).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual([]); + expect(targetIds).toEqual(['targetId']); expect(action).toEqual(['action']); }); it(`should return false when missing event.action`, () => { + hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { + initialProps: { + getFieldsData: mockGetFieldsData, + ecsData: { + _id: 'id', + }, + }, + }); + + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; + expect(isAuditLog).toEqual(false); + expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); + expect(eventIds).toEqual(['eventId']); + expect(actorIds).toEqual(['actorId']); + expect(targetIds).toEqual(['targetId']); + expect(action).toEqual(undefined); + }); + + it(`should return false when missing target`, () => { const getFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { - return 'eventId'; - } else if (field === 'actor.entity.id') { - return 'actorId'; + if (field === 'target.entity.id') { + return; } - return mockFieldData[field]; + return mockGetFieldsData(field); }; hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { @@ -62,20 +94,23 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; expect(isAuditLog).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); + expect(targetIds).toEqual([]); expect(action).toEqual(undefined); }); it(`should return false when missing original_event.id`, () => { const getFieldsData: GetFieldsData = (field: string) => { - if (field === 'actor.entity.id') { - return 'actorId'; + if (field === 'kibana.alert.original_event.id') { + return; } - return mockFieldData[field]; + + return mockGetFieldsData(field); }; hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { @@ -90,11 +125,13 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; expect(isAuditLog).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual([]); expect(actorIds).toEqual(['actorId']); + expect(targetIds).toEqual(['targetId']); expect(action).toEqual(['action']); }); @@ -102,13 +139,9 @@ describe('useGraphPreview', () => { const getFieldsData: GetFieldsData = (field: string) => { if (field === '@timestamp') { return; - } else if (field === 'kibana.alert.original_event.id') { - return 'eventId'; - } else if (field === 'actor.entity.id') { - return 'actorId'; } - return mockFieldData[field]; + return mockGetFieldsData(field); }; hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { @@ -123,28 +156,20 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; expect(isAuditLog).toEqual(false); expect(timestamp).toEqual(null); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); + expect(targetIds).toEqual(['targetId']); expect(action).toEqual(['action']); }); it(`should return true when alert is has graph preview`, () => { - const getFieldsData: GetFieldsData = (field: string) => { - if (field === 'kibana.alert.original_event.id') { - return 'eventId'; - } else if (field === 'actor.entity.id') { - return 'actorId'; - } - - return mockFieldData[field]; - }; - hookResult = renderHook((props: UseGraphPreviewParams) => useGraphPreview(props), { initialProps: { - getFieldsData, + getFieldsData: mockGetFieldsData, ecsData: { _id: 'id', event: { @@ -154,11 +179,13 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; expect(isAuditLog).toEqual(true); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); + expect(targetIds).toEqual(['targetId']); expect(action).toEqual(['action']); }); @@ -168,6 +195,8 @@ describe('useGraphPreview', () => { return ['id1', 'id2']; } else if (field === 'actor.entity.id') { return ['actorId1', 'actorId2']; + } else if (field === 'target.entity.id') { + return ['targetId1', 'targetId2']; } return mockFieldData[field]; @@ -185,11 +214,13 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action } = hookResult.result.current; + const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + hookResult.result.current; expect(isAuditLog).toEqual(true); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['id1', 'id2']); expect(actorIds).toEqual(['actorId1', 'actorId2']); expect(action).toEqual(['action1', 'action2']); + expect(targetIds).toEqual(['targetId1', 'targetId2']); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts index bbaeb808c9e2a..c4d1276ab86b3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts @@ -40,6 +40,11 @@ export interface UseGraphPreviewResult { */ actorIds: string[]; + /** + * Array of target entity IDs associated with the alert + */ + targetIds: string[]; + /** * Action associated with the event */ @@ -64,9 +69,14 @@ export const useGraphPreview = ({ const eventIds = originalEventId ? getFieldArray(originalEventId) : getFieldArray(eventId); const actorIds = getFieldArray(getFieldsData('actor.entity.id')); + const targetIds = getFieldArray(getFieldsData('target.entity.id')); const action: string[] | undefined = get(['event', 'action'], ecsData); const isAuditLog = - Boolean(timestamp) && actorIds.length > 0 && Boolean(action?.length) && eventIds.length > 0; + Boolean(timestamp) && + Boolean(action?.length) && + actorIds.length > 0 && + eventIds.length > 0 && + targetIds.length > 0; - return { timestamp, eventIds, actorIds, action, isAuditLog }; + return { timestamp, eventIds, actorIds, action, targetIds, isAuditLog }; }; diff --git a/x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit/data.json b/x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit/data.json index 5e3d4cdfdffd5..e5b83d55c15ae 100644 --- a/x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit/data.json +++ b/x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit/data.json @@ -90,6 +90,11 @@ "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" }, "related": { + "entity": [ + "10.0.0.1", + "projects/your-project-id/roles/customRole", + "admin@example.com" + ], "ip": [ "10.0.0.1" ], @@ -215,6 +220,11 @@ "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" }, "related": { + "entity": [ + "10.0.0.1", + "projects/your-project-id/roles/customRole", + "admin2@example.com" + ], "ip": [ "10.0.0.1" ], @@ -340,6 +350,11 @@ "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" }, "related": { + "entity": [ + "10.0.0.1", + "projects/your-project-id/roles/customRole", + "admin3@example.com" + ], "ip": [ "10.0.0.1" ], @@ -465,6 +480,11 @@ "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" }, "related": { + "entity": [ + "10.0.0.1", + "projects/your-project-id/roles/customRole", + "admin3@example.com" + ], "ip": [ "10.0.0.1" ], @@ -599,6 +619,11 @@ "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" }, "related": { + "entity": [ + "10.0.0.1", + "projects/your-project-id/roles/customRole", + "admin4@example.com" + ], "ip": [ "10.0.0.1" ], From 3821ad9c5f35eaa9b78301aa9090853755b725d0 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 18 Nov 2024 16:33:51 +0000 Subject: [PATCH 17/36] Updated shared hooks --- .../document_details/left/components/graph_visualization.tsx | 4 ++-- .../flyout/document_details/left/tabs/visualize_tab.tsx | 2 +- .../right/components/graph_preview_container.tsx | 4 ++-- .../right/components/visualizations_section.tsx | 2 +- .../{right => shared}/hooks/use_fetch_graph_data.test.tsx | 0 .../{right => shared}/hooks/use_fetch_graph_data.ts | 0 .../{right => shared}/hooks/use_graph_preview.test.tsx | 4 ++-- .../{right => shared}/hooks/use_graph_preview.ts | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) rename x-pack/plugins/security_solution/public/flyout/document_details/{right => shared}/hooks/use_fetch_graph_data.test.tsx (100%) rename x-pack/plugins/security_solution/public/flyout/document_details/{right => shared}/hooks/use_fetch_graph_data.ts (100%) rename x-pack/plugins/security_solution/public/flyout/document_details/{right => shared}/hooks/use_graph_preview.test.tsx (97%) rename x-pack/plugins/security_solution/public/flyout/document_details/{right => shared}/hooks/use_graph_preview.ts (94%) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index 94f8137d8a4ac..d7983105d5a0e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -23,8 +23,8 @@ import { useGetScopedSourcererDataView } from '../../../../sourcerer/components/ import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_VISUALIZATION_TEST_ID } from './test_ids'; -import { useFetchGraphData } from '../../right/hooks/use_fetch_graph_data'; -import { useGraphPreview } from '../../right/hooks/use_graph_preview'; +import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; +import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; export const GRAPH_VISUALIZATION_ID = 'graph_visualization'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index fd810335e5256..49b8d6178e6c3 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -30,7 +30,7 @@ import { SESSION_VIEW_ID, SessionView } from '../components/session_view'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { GRAPH_VISUALIZATION_ID, GraphVisualization } from '../components/graph_visualization'; -import { useGraphPreview } from '../../right/hooks/use_graph_preview'; +import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE } from '../../shared/constants/feature_flags'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 6a1533e280815..12bde04830e13 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -12,8 +12,8 @@ import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/c import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; import { GraphPreview } from './graph_preview'; -import { useFetchGraphData } from '../hooks/use_fetch_graph_data'; -import { useGraphPreview } from '../hooks/use_graph_preview'; +import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; +import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useNavigateToGraphVisualization } from '../../shared/hooks/use_navigate_to_graph_visualization'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index ef7d9c3ff9c56..5f772f668812a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -16,7 +16,7 @@ import { VISUALIZATIONS_TEST_ID } from './test_ids'; import { GraphPreviewContainer } from './graph_preview_container'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useGraphPreview } from '../hooks/use_graph_preview'; +import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE } from '../../shared/constants/feature_flags'; const KEY = 'visualizations'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.ts similarity index 100% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.ts diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx similarity index 97% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx index a69040be9c458..679b631acf709 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.test.tsx @@ -9,8 +9,8 @@ import type { RenderHookResult } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks'; import type { UseGraphPreviewParams, UseGraphPreviewResult } from './use_graph_preview'; import { useGraphPreview } from './use_graph_preview'; -import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; -import { mockFieldData } from '../../shared/mocks/mock_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; +import { mockFieldData } from '../mocks/mock_get_fields_data'; const mockGetFieldsData: GetFieldsData = (field: string) => { if (field === 'kibana.alert.original_event.id') { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts similarity index 94% rename from x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts index c4d1276ab86b3..d41e905c80703 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_graph_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts @@ -7,8 +7,8 @@ import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { get } from 'lodash/fp'; -import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; -import { getField, getFieldArray } from '../../shared/utils'; +import type { GetFieldsData } from './use_get_fields_data'; +import { getField, getFieldArray } from '../utils'; export interface UseGraphPreviewParams { /** From 33134f6da5aa87312fc19b776984c269c1244d3a Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 19 Nov 2024 16:07:56 +0000 Subject: [PATCH 18/36] Switched feature flag to an existing advanced setting --- .../server/routes/graph/route.ts | 19 +++++-- .../cloud_security_posture/server/types.ts | 2 + .../common/experimental_features.ts | 5 -- .../left/tabs/visualize_tab.tsx | 9 +--- .../graph_preview_container.test.tsx | 8 +-- .../visualizations_section.test.tsx | 19 +++---- .../components/visualizations_section.tsx | 14 +++--- .../shared/constants/feature_flags.ts | 9 ---- .../security_solution/server/ui_settings.ts | 2 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apis/cloud_security_posture/config.ts | 21 ++++++-- .../apis/cloud_security_posture/graph.ts | 50 +++++++++++++++++++ .../apis/cloud_security_posture/index.ts | 1 + .../test/cloud_security_posture_api/config.ts | 1 + .../config.ts | 4 +- .../page_objects/alerts_page.ts | 6 +-- 18 files changed, 113 insertions(+), 60 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts create mode 100644 x-pack/test/api_integration/apis/cloud_security_posture/graph.ts diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts index 9fb817b275a0d..fd42061472044 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -12,9 +12,12 @@ import { import { transformError } from '@kbn/securitysolution-es-utils'; import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1'; import { GRAPH_ROUTE_PATH } from '../../../common/constants'; -import { CspRouter } from '../../types'; +import { CspRequestHandlerContext, CspRouter } from '../../types'; import { getGraph as getGraphV1 } from './v1'; +const ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING = + 'securitySolution:enableVisualizationsInFlyout' as const; + export const defineGraphRoute = (router: CspRouter) => router.versioned .post({ @@ -39,10 +42,20 @@ export const defineGraphRoute = (router: CspRouter) => }, }, }, - async (context, request, response) => { + async (context: CspRequestHandlerContext, request, response) => { + const cspContext = await context.csp; + const isGraphEnabled = await ( + await context.core + ).uiSettings.client.get(ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING); + + cspContext.logger.debug(`isGraphEnabled: ${isGraphEnabled}`); + + if (!isGraphEnabled) { + return response.notFound(); + } + const { nodesLimit, showUnknownTarget = false } = request.body; const { eventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; - const cspContext = await context.csp; const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id; try { diff --git a/x-pack/plugins/cloud_security_posture/server/types.ts b/x-pack/plugins/cloud_security_posture/server/types.ts index df812fe534722..d5dc022bfcf66 100644 --- a/x-pack/plugins/cloud_security_posture/server/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/types.ts @@ -22,6 +22,7 @@ import type { Logger, SavedObjectsClientContract, IScopedClusterClient, + CoreRequestHandlerContext, } from '@kbn/core/server'; import type { AgentService, @@ -88,6 +89,7 @@ export type CspRequestHandlerContext = CustomRequestHandlerContext<{ csp: CspApiRequestHandlerContext; fleet: FleetRequestHandlerContext['fleet']; alerting: AlertingApiRequestHandlerContext; + core: Promise; }>; /** diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index dc6495e1d9737..c6dbfec6a139f 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -185,11 +185,6 @@ export const allowedExperimentalValues = Object.freeze({ */ analyzerDatePickersAndSourcererDisabled: false, - /** - * Enables graph visualization in alerts flyout - */ - graphVisualizationInFlyoutEnabled: false, - /** * Enables an ability to customize Elastic prebuilt rules. * diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 49b8d6178e6c3..599cc8e017a2f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -11,7 +11,6 @@ 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 { useDocumentDetailsContext } from '../../shared/context'; import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; @@ -31,7 +30,6 @@ import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { GRAPH_VISUALIZATION_ID, GraphVisualization } from '../components/graph_visualization'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE } from '../../shared/constants/feature_flags'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ { @@ -102,10 +100,6 @@ export const VisualizeTab = memo(() => { } }, [panels.left?.path?.subTab]); - const graphVisualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( - GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE - ); - // Decide whether to show the graph preview or not const { isAuditLog: isGraphPreviewEnabled } = useGraphPreview({ getFieldsData, @@ -114,8 +108,9 @@ export const VisualizeTab = memo(() => { const options = [...visualizeButtons]; - if (graphVisualizationInFlyoutEnabled && isGraphPreviewEnabled) + if (isGraphPreviewEnabled) { options.push(graphVisualizationButton); + } return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index ae907af316dc9..d6cbb7a2f55fc 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -12,8 +12,8 @@ import { DocumentDetailsContext } from '../../shared/context'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { GraphPreviewContainer } from './graph_preview_container'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; -import { useGraphPreview } from '../hooks/use_graph_preview'; -import { useFetchGraphData } from '../hooks/use_fetch_graph_data'; +import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; +import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID, EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID, @@ -22,8 +22,8 @@ import { EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '../../../shared/components/test_ids'; -jest.mock('../hooks/use_graph_preview'); -jest.mock('../hooks/use_fetch_graph_data', () => ({ +jest.mock('../../shared/hooks/use_graph_preview'); +jest.mock('../../shared/hooks/use_fetch_graph_data', () => ({ useFetchGraphData: jest.fn(), })); const mockUseFetchGraphData = useFetchGraphData as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 3aeb7d30f8e48..fef1d72c0df19 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -25,9 +25,8 @@ import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { useExpandSection } from '../hooks/use_expand_section'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useGraphPreview } from '../hooks/use_graph_preview'; -import { useFetchGraphData } from '../hooks/use_fetch_graph_data'; +import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; +import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; jest.mock('../hooks/use_expand_section'); jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ @@ -53,11 +52,6 @@ jest.mock( jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); -jest.mock('../../../../common/hooks/use_experimental_features', () => ({ - useIsExperimentalFeatureEnabled: jest.fn(), -})); - -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; const mockUseUiSetting = jest.fn().mockReturnValue([false]); jest.mock('@kbn/kibana-react-plugin/public', () => { @@ -67,11 +61,11 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { useUiSetting$: () => mockUseUiSetting(), }; }); -jest.mock('../hooks/use_graph_preview'); +jest.mock('../../shared/hooks/use_graph_preview'); const mockUseGraphPreview = useGraphPreview as jest.Mock; -jest.mock('../hooks/use_fetch_graph_data', () => ({ +jest.mock('../../shared/hooks/use_fetch_graph_data', () => ({ useFetchGraphData: jest.fn(), })); @@ -95,6 +89,7 @@ const renderVisualizationsSection = (contextValue = panelContextValue) => describe('', () => { beforeEach(() => { + mockUseUiSetting.mockReturnValue([false]); mockUseTimelineDataFilters.mockReturnValue({ selectedPatterns: ['index'] }); mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ loading: false, @@ -136,7 +131,7 @@ describe('', () => { }); (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); (useExpandSection as jest.Mock).mockReturnValue(true); - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + mockUseUiSetting.mockReturnValue([false]); const { getByTestId, queryByTestId } = renderVisualizationsSection(); expect(getByTestId(VISUALIZATIONS_SECTION_CONTENT_TEST_ID)).toBeVisible(); @@ -148,7 +143,7 @@ describe('', () => { it('should render the graph preview component if the feature is enabled', () => { (useExpandSection as jest.Mock).mockReturnValue(true); - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + mockUseUiSetting.mockReturnValue([true]); const { getByTestId } = renderVisualizationsSection(); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index 5f772f668812a..12ed787ae9c80 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -8,16 +8,16 @@ import React, { memo } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { useExpandSection } from '../hooks/use_expand_section'; import { AnalyzerPreviewContainer } from './analyzer_preview_container'; import { SessionPreviewContainer } from './session_preview_container'; import { ExpandableSection } from './expandable_section'; import { VISUALIZATIONS_TEST_ID } from './test_ids'; import { GraphPreviewContainer } from './graph_preview_container'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useDocumentDetailsContext } from '../../shared/context'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE } from '../../shared/constants/feature_flags'; +import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; const KEY = 'visualizations'; @@ -26,12 +26,12 @@ const KEY = 'visualizations'; */ export const VisualizationsSection = memo(() => { const expanded = useExpandSection({ title: KEY, defaultValue: false }); - const graphVisualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( - GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE - ); - const { dataAsNestedObject, getFieldsData } = useDocumentDetailsContext(); + const [visualizationInFlyoutEnabled] = useUiSetting$( + ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING + ); + // Decide whether to show the graph preview or not const { isAuditLog: isGraphPreviewEnabled } = useGraphPreview({ getFieldsData, @@ -53,7 +53,7 @@ export const VisualizationsSection = memo(() => { - {graphVisualizationInFlyoutEnabled && isGraphPreviewEnabled && ( + {visualizationInFlyoutEnabled && isGraphPreviewEnabled && ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts deleted file mode 100644 index 79a6479782ca1..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/feature_flags.ts +++ /dev/null @@ -1,9 +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. - */ - -export const GRAPH_VISUALIZATION_EXPERIMENTAL_FEATURE = - 'graphVisualizationInFlyoutEnabled' as const; diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 6aeb86750be7a..592c648792090 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -209,7 +209,7 @@ export const initUiSettings = ( 'xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription', { defaultMessage: - '[technical preview] Enable visualizations (analyzer and session viewer) in flyout.', + '[technical preview] Enable visualizations (analyzer, session viewer, and graph view) in flyout.', values: { em: (chunks) => `${chunks}` }, } ), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 27d974d49bcaa..8cd078fb02271 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -42930,7 +42930,6 @@ "xpack.securitySolution.uiSettings.enableCcsWarningDescription": "

Active les avertissements de vérification des privilèges dans les règles relatives aux index CCS

", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

Active le fil d'actualités

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "Fil d'actualités", - "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription": "[version d'évaluation technique] Activez les visualisations (analyseur et visualiseur de session) dans le menu volant.", "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel": "Activer les visualisations dans le menu volant", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzer": "Exclure les niveaux froids de l'analyseur", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzerDescription": "

Lorsque cette option est activée, les niveaux \"cold\" et \"frozen\" sont ignorés dans les requêtes de l'analyseur

", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 722631f102426..89dcc751902e6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -42896,7 +42896,6 @@ "xpack.securitySolution.uiSettings.enableCcsWarningDescription": "

CCSインデックスのルールで権限チェック警告を有効にします

", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

ニュースフィードを有効にします

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "ニュースフィード", - "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription": "[テクニカルプレビュー]フライアウトで視覚化(アナライザーとセッションビューアー)を有効にします。", "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel": "フライアウトでビジュアライゼーションを有効化", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzer": "アナライザーでコールドティアとフローズンティアを除外", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzerDescription": "

有効にすると、アナライザークエリーでコールドティアとフローズンティアがスキップされます

", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c666d4e60b726..30a30de1df3d2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -42242,7 +42242,6 @@ "xpack.securitySolution.uiSettings.enableCcsWarningDescription": "

在规则中为 CCS 索引启用权限检查警告

", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

启用新闻源

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "新闻源", - "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription": "[技术预览] 在浮出控件中启用可视化(分析器和会话查看器)。", "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel": "在浮出控件中启用可视化", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzer": "在分析器中排除冷层和冻结层", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzerDescription": "

启用后,将在分析器查询中跳过冷层和冻结层

", diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/config.ts b/x-pack/test/api_integration/apis/cloud_security_posture/config.ts index 5f335f116fefe..81a2cd0df2fd5 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/config.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/config.ts @@ -5,13 +5,28 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import { FtrConfigProviderContext, getKibanaCliLoggers } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + const baseConfig = await readConfigFile(require.resolve('../../config.ts')); return { - ...baseIntegrationTestsConfig.getAll(), + ...baseConfig.getAll(), testFiles: [require.resolve('.')], + kbnTestServer: { + ...baseConfig.get('kbnTestServer'), + serverArgs: [ + ...baseConfig.get('kbnTestServer.serverArgs'), + `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=false`, // Disables /graph API + `--logging.loggers=${JSON.stringify([ + ...getKibanaCliLoggers(baseConfig.get('kbnTestServer.serverArgs')), + { + name: 'plugins.cloudSecurityPosture', + level: 'all', + appenders: ['default'], + }, + ])}`, + ], + }, }; } diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts new file mode 100644 index 0000000000000..561c14de1b2ff --- /dev/null +++ b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts @@ -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 { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import type { Agent } from 'supertest'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/latest'; +import { FtrProviderContext } from '@kbn/ftr-common-functional-services'; +import { result } from '../../../cloud_security_posture_api/utils'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + + const logger = getService('log'); + const supertest = getService('supertest'); + + const postGraph = (agent: Agent, body: GraphRequest, auth?: { user: string; pass: string }) => { + const req = agent + .post('/internal/cloud_security_posture/graph') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx'); + + if (auth) { + req.auth(auth.user, auth.pass); + } + + return req.send(body); + }; + + describe('POST /internal/cloud_security_posture/graph', () => { + describe('Feature flag', () => { + it('should return 404 when feature flag is not toggled', async () => { + await postGraph(supertest, { + query: { + eventIds: [], + start: 'now-1d/d', + end: 'now/d', + }, + }).expect(result(404, logger)); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/index.ts b/x-pack/test/api_integration/apis/cloud_security_posture/index.ts index fa11aab67b279..46f594be3ed38 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/index.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/index.ts @@ -20,6 +20,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./benchmark/v2')); loadTestFile(require.resolve('./rules/v1')); loadTestFile(require.resolve('./rules/v2')); + loadTestFile(require.resolve('./graph')); // Place your tests files under this directory and add the following here: // loadTestFile(require.resolve('./your test name')); diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index 4e0ecd1f26e43..0dac59b47c52c 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -34,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { * 1. release a new package to EPR * 2. merge the updated version number change to kibana */ + `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=true`, // Enables /graph API `--xpack.fleet.packages.0.name=cloud_security_posture`, `--xpack.fleet.packages.0.version=${CLOUD_SECURITY_PLUGIN_VERSION}`, // `--xpack.fleet.registryUrl=https://localhost:8080`, diff --git a/x-pack/test/cloud_security_posture_functional/config.ts b/x-pack/test/cloud_security_posture_functional/config.ts index 7e80788ffccfe..db560992e4fad 100644 --- a/x-pack/test/cloud_security_posture_functional/config.ts +++ b/x-pack/test/cloud_security_posture_functional/config.ts @@ -38,9 +38,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { * 1. release a new package to EPR * 2. merge the updated version number change to kibana */ - `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - 'graphVisualizationInFlyoutEnabled', - ])}`, + `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=true`, `--xpack.fleet.packages.0.name=cloud_security_posture`, `--xpack.fleet.packages.0.version=${CLOUD_SECURITY_PLUGIN_VERSION}`, // `--xpack.fleet.registryUrl=https://localhost:8080`, diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/alerts_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/alerts_page.ts index f3a9f7b1448a8..6ebd496fca365 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/alerts_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/alerts_page.ts @@ -10,7 +10,7 @@ import { FtrService } from '../../functional/ftr_provider_context'; const ALERT_TABLE_ROW_CSS_SELECTOR = '[data-test-subj="alertsTable"] .euiDataGridRow'; const VISUALIZATIONS_SECTION_HEADER_TEST_ID = 'securitySolutionFlyoutVisualizationsHeader'; -const GRAPH_PREVIEW_TEST_ID = 'securitySolutionFlyoutGraphPreview'; +const GRAPH_PREVIEW_CONTENT_TEST_ID = 'securitySolutionFlyoutGraphPreviewContent'; const GRAPH_PREVIEW_LOADING_TEST_ID = 'securitySolutionFlyoutGraphPreviewLoading'; export class AlertsPageObject extends FtrService { @@ -89,12 +89,12 @@ export class AlertsPageObject extends FtrService { }, assertGraphPreviewVisible: async () => { - return await this.testSubjects.existOrFail(GRAPH_PREVIEW_TEST_ID); + return await this.testSubjects.existOrFail(GRAPH_PREVIEW_CONTENT_TEST_ID); }, assertGraphNodesNumber: async (expected: number) => { await this.flyout.waitGraphIsLoaded(); - const graph = await this.testSubjects.find(GRAPH_PREVIEW_TEST_ID); + const graph = await this.testSubjects.find(GRAPH_PREVIEW_CONTENT_TEST_ID); await graph.scrollIntoView(); const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); expect(nodes.length).to.be(expected); From 708300d2a2d7b21340a7a07932a3eb24d553117b Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Wed, 20 Nov 2024 16:53:48 +0000 Subject: [PATCH 19/36] Added FTR to test e2e expanded flyout filtering --- .../components/node/node_expand_button.tsx | 1 + .../components/graph_node_expand_popover.tsx | 1 + .../left/components/graph_visualization.tsx | 5 +- .../page_objects/expanded_flyout.ts | 112 ++++++++++++++++++ .../page_objects/index.ts | 4 + .../page_objects/network_events_page.ts | 107 +++++++++++++++++ .../pages/alerts_flyout.ts | 48 +++++++- .../pages/events_flyout.ts | 98 +++++++++++++++ .../pages/index.ts | 1 + 9 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts create mode 100644 x-pack/test/cloud_security_posture_functional/page_objects/network_events_page.ts create mode 100644 x-pack/test/cloud_security_posture_functional/pages/events_flyout.ts diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx index 522b7d1b5d45b..07581c1e3d3dd 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx @@ -35,6 +35,7 @@ export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { onClick={onClickHandler} iconSize="m" aria-label="Open or close node actions" + data-test-subj="nodeExpandButton" /> ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx index fd22315f3562e..7389493ef06df 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx @@ -41,6 +41,7 @@ export const GraphNodeExpandPopover: React.FC = mem isOpen={isOpen} anchorElement={anchorElement} closePopover={closePopover} + data-test-subj="graphNodeExpandPopover" > ({ +const buildPhraseFilter = (field: string, value: string, dataViewId?: string): PhraseFilter => ({ meta: { key: field, + index: dataViewId, negate: false, disabled: false, type: 'phrase', @@ -110,7 +111,7 @@ const addFilter = (dataViewId: string, prev: Filter[], key: string, value: strin ...otherFilters, ]; } else { - return [buildPhraseFilter(key, value), ...prev]; + return [buildPhraseFilter(key, value, dataViewId), ...prev]; } }; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts b/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts new file mode 100644 index 0000000000000..062425f939477 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts @@ -0,0 +1,112 @@ +/* + * 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 expect from '@kbn/expect'; +import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; +import type { FilterBarService } from '@kbn/test-suites-src/functional/services/filter_bar'; +import { FtrService } from '../../functional/ftr_provider_context'; + +const GRAPH_PREVIEW_TITLE_LINK_TEST_ID = 'securitySolutionFlyoutGraphPreviewTitleLink'; +const GRAPH_VISUALIZATION_TEST_ID = 'securitySolutionFlyoutGraphVisualization'; +const NODE_EXPAND_BUTTON_TEST_ID = 'nodeExpandButton'; +const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = 'graphNodeExpandPopover'; +const GRAPH_NODE_POPOVER_EXPLORE_RELATED_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}ExploreRelatedEntities`; +const GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}ShowActionsByEntity`; +const GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}ShowActionsOnEntity`; +type Filter = Parameters[0]; + +export class ExpandedFlyout extends FtrService { + private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly filterBar = this.ctx.getService('filterBar'); + + async expandGraph(): Promise { + await this.testSubjects.click(GRAPH_PREVIEW_TITLE_LINK_TEST_ID); + } + + async waitGraphIsLoaded(): Promise { + await this.testSubjects.existOrFail(GRAPH_VISUALIZATION_TEST_ID, { timeout: 10000 }); + } + + async assertGraphNodesNumber(expected: number): Promise { + await this.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_VISUALIZATION_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); + expect(nodes.length).to.be(expected); + } + + async selectNode(nodeId: string): Promise { + await this.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_VISUALIZATION_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector( + `.react-flow__nodes .react-flow__node[data-id="${nodeId}"]` + ); + expect(nodes.length).to.be(1); + await nodes[0].moveMouseTo(); + return nodes[0]; + } + + async clickOnNodeExpandButton(nodeId: string): Promise { + const node = await this.selectNode(nodeId); + const expandButton = await node.findByTestSubject(NODE_EXPAND_BUTTON_TEST_ID); + await expandButton.click(); + await this.testSubjects.existOrFail(GRAPH_NODE_EXPAND_POPOVER_TEST_ID); + } + + async showActionsByEntity(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async showActionsOnEntity(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async exploreRelatedEntities(nodeId: string): Promise { + await this.clickOnNodeExpandButton(nodeId); + await this.testSubjects.click(GRAPH_NODE_POPOVER_EXPLORE_RELATED_TEST_ID); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async expectFilterTextEquals(filterIdx: number, expected: string): Promise { + const filters = await this.filterBar.getFiltersLabel(); + expect(filters.length).to.be.greaterThan(filterIdx); + expect(filters[filterIdx]).to.be(expected); + } + + async expectFilterPreviewEquals(filterIdx: number, expected: string): Promise { + await this.clickEditFilter(filterIdx); + + const filterPreview = await this.filterBar.getFilterEditorPreview(); + expect(filterPreview).to.be(expected); + + await this.filterBar.ensureFieldEditorModalIsClosed(); + } + + async clickEditFilter(filterIdx: number): Promise { + await this.filterBar.clickEditFilterById(filterIdx.toString()); + } + + async clearAllFilters(): Promise { + await this.testSubjects.click(`${GRAPH_VISUALIZATION_TEST_ID} > showQueryBarMenu`); + await this.testSubjects.click('filter-sets-removeAllFilters'); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + async addFilter(filter: Filter): Promise { + await this.testSubjects.click(`${GRAPH_VISUALIZATION_TEST_ID} > addFilter`); + await this.filterBar.createFilter(filter); + await this.testSubjects.scrollIntoView('saveFilter'); + await this.testSubjects.clickWhenNotDisabled('saveFilter'); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } +} diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts index b7c20632e82f5..fdc904e31aac0 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts @@ -14,9 +14,13 @@ import { BenchmarkPagePageProvider } from './benchmark_page'; import { CspSecurityCommonProvider } from './security_common'; import { RulePagePageProvider } from './rule_page'; import { AlertsPageObject } from './alerts_page'; +import { NetworkEventsPageObject } from './network_events_page'; +import { ExpandedFlyout } from './expanded_flyout'; export const cloudSecurityPosturePageObjects = { alerts: AlertsPageObject, + networkEvents: NetworkEventsPageObject, + expandedFlyout: ExpandedFlyout, findings: FindingsPageProvider, cloudPostureDashboard: CspDashboardPageProvider, cisAddIntegration: AddCisIntegrationFormPageProvider, diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/network_events_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/network_events_page.ts new file mode 100644 index 0000000000000..8e03fae7eb7e0 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/page_objects/network_events_page.ts @@ -0,0 +1,107 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrService } from '../../functional/ftr_provider_context'; + +const EVENTS_TABLE_ROW_CSS_SELECTOR = '[data-test-subj="events-viewer-panel"] .euiDataGridRow'; +const VISUALIZATIONS_SECTION_HEADER_TEST_ID = 'securitySolutionFlyoutVisualizationsHeader'; +const GRAPH_PREVIEW_CONTENT_TEST_ID = 'securitySolutionFlyoutGraphPreviewContent'; +const GRAPH_PREVIEW_LOADING_TEST_ID = 'securitySolutionFlyoutGraphPreviewLoading'; + +export class NetworkEventsPageObject extends FtrService { + private readonly retry = this.ctx.getService('retry'); + private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly defaultTimeoutMs = this.ctx.getService('config').get('timeouts.waitFor'); + + async navigateToNetworkEventsPage(urlQueryParams: string = ''): Promise { + await this.pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolution', + '/network/events', + `${urlQueryParams && `?${urlQueryParams}`}`, + { + ensureCurrentUrl: false, + } + ); + await this.pageObjects.header.waitUntilLoadingHasFinished(); + } + + getAbsoluteTimerangeFilter(from: string, to: string) { + return `timerange=(global:(linkTo:!(),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`; + } + + getFlyoutFilter(eventId: string) { + return `flyout=(preview:!(),right:(id:document-details-right,params:(id:%27${eventId}%27,indexName:logs-gcp.audit-default,scopeId:network-page-events)))`; + } + + /** + * Clicks the refresh button on the network events page and waits for it to complete + */ + async clickRefresh(): Promise { + await this.ensureOnNetworkEventsPage(); + await this.testSubjects.click('querySubmitButton'); + + // wait for refresh to complete + await this.retry.waitFor( + 'Network events pages refresh button to be enabled', + async (): Promise => { + const refreshButton = await this.testSubjects.find('querySubmitButton'); + + return (await refreshButton.isDisplayed()) && (await refreshButton.isEnabled()); + } + ); + } + + async ensureOnNetworkEventsPage(): Promise { + await this.testSubjects.existOrFail('network-details-headline'); + } + + async waitForListToHaveEvents(timeoutMs?: number): Promise { + const allEventRows = await this.testSubjects.findService.allByCssSelector( + EVENTS_TABLE_ROW_CSS_SELECTOR + ); + + if (!Boolean(allEventRows.length)) { + await this.retry.waitForWithTimeout( + 'waiting for events to show up on network events page', + timeoutMs ?? this.defaultTimeoutMs, + async (): Promise => { + await this.clickRefresh(); + + const allEventRowsInner = await this.testSubjects.findService.allByCssSelector( + EVENTS_TABLE_ROW_CSS_SELECTOR + ); + + return Boolean(allEventRowsInner.length); + } + ); + } + } + + flyout = { + expandVisualizations: async (): Promise => { + await this.testSubjects.click(VISUALIZATIONS_SECTION_HEADER_TEST_ID); + }, + + assertGraphPreviewVisible: async () => { + return await this.testSubjects.existOrFail(GRAPH_PREVIEW_CONTENT_TEST_ID); + }, + + assertGraphNodesNumber: async (expected: number) => { + await this.flyout.waitGraphIsLoaded(); + const graph = await this.testSubjects.find(GRAPH_PREVIEW_CONTENT_TEST_ID); + await graph.scrollIntoView(); + const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); + expect(nodes.length).to.be(expected); + }, + + waitGraphIsLoaded: async () => { + await this.testSubjects.missingOrFail(GRAPH_PREVIEW_LOADING_TEST_ID, { timeout: 10000 }); + }, + }; +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts b/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts index 63eafc4107bc1..35f9578929ada 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/alerts_flyout.ts @@ -14,8 +14,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const logger = getService('log'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const pageObjects = getPageObjects(['common', 'header', 'alerts']); + const pageObjects = getPageObjects(['common', 'header', 'alerts', 'expandedFlyout']); const alertsPage = pageObjects.alerts; + const expandedFlyout = pageObjects.expandedFlyout; describe('Security Alerts Page - Graph visualization', function () { this.tags(['cloud_security_posture_graph_viz']); @@ -54,9 +55,52 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); }); - it('should render graph visualization', async () => { + it('expanded flyout - filter by node', async () => { await alertsPage.flyout.assertGraphPreviewVisible(); await alertsPage.flyout.assertGraphNodesNumber(3); + + await expandedFlyout.expandGraph(); + await expandedFlyout.waitGraphIsLoaded(); + await expandedFlyout.assertGraphNodesNumber(3); + + // Show actions by entity + await expandedFlyout.showActionsByEntity('admin@example.com'); + await expandedFlyout.expectFilterTextEquals(0, 'actor.entity.id: admin@example.com'); + await expandedFlyout.expectFilterPreviewEquals(0, 'actor.entity.id: admin@example.com'); + + // Show actions on entity + await expandedFlyout.showActionsOnEntity('admin@example.com'); + await expandedFlyout.expectFilterTextEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com' + ); + await expandedFlyout.expectFilterPreviewEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com' + ); + + // Explore related entities + await expandedFlyout.exploreRelatedEntities('admin@example.com'); + await expandedFlyout.expectFilterTextEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com OR related.entity: admin@example.com' + ); + await expandedFlyout.expectFilterPreviewEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com OR related.entity: admin@example.com' + ); + + // Clear filters + await expandedFlyout.clearAllFilters(); + + // Add custom filter + await expandedFlyout.addFilter({ + field: 'actor.entity.id', + operation: 'is', + value: 'admin2@example.com', + }); + await pageObjects.header.waitUntilLoadingHasFinished(); + await expandedFlyout.assertGraphNodesNumber(5); }); }); } diff --git a/x-pack/test/cloud_security_posture_functional/pages/events_flyout.ts b/x-pack/test/cloud_security_posture_functional/pages/events_flyout.ts new file mode 100644 index 0000000000000..0848307ca26d2 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/events_flyout.ts @@ -0,0 +1,98 @@ +/* + * 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 { waitForPluginInitialized } from '../../cloud_security_posture_api/utils'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const retry = getService('retry'); + const logger = getService('log'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const pageObjects = getPageObjects(['common', 'header', 'networkEvents', 'expandedFlyout']); + const networkEventsPage = pageObjects.networkEvents; + const expandedFlyout = pageObjects.expandedFlyout; + + describe('Security Network Page - Graph visualization', function () { + this.tags(['cloud_security_posture_graph_viz']); + + before(async () => { + await esArchiver.load( + 'x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit' + ); + + await waitForPluginInitialized({ retry, supertest, logger }); + + // Setting the timerange to fit the data and open the flyout for a specific alert + await networkEventsPage.navigateToNetworkEventsPage( + `${networkEventsPage.getAbsoluteTimerangeFilter( + '2024-09-01T00:00:00.000Z', + '2024-09-02T00:00:00.000Z' + )}&${networkEventsPage.getFlyoutFilter('1')}` + ); + + await networkEventsPage.waitForListToHaveEvents(); + + await networkEventsPage.flyout.expandVisualizations(); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit' + ); + }); + + it('expanded flyout - filter by node', async () => { + await networkEventsPage.flyout.assertGraphPreviewVisible(); + await networkEventsPage.flyout.assertGraphNodesNumber(3); + + await expandedFlyout.expandGraph(); + await expandedFlyout.waitGraphIsLoaded(); + await expandedFlyout.assertGraphNodesNumber(3); + + // Show actions by entity + await expandedFlyout.showActionsByEntity('admin@example.com'); + await expandedFlyout.expectFilterTextEquals(0, 'actor.entity.id: admin@example.com'); + await expandedFlyout.expectFilterPreviewEquals(0, 'actor.entity.id: admin@example.com'); + + // Show actions on entity + await expandedFlyout.showActionsOnEntity('admin@example.com'); + await expandedFlyout.expectFilterTextEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com' + ); + await expandedFlyout.expectFilterPreviewEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com' + ); + + // Explore related entities + await expandedFlyout.exploreRelatedEntities('admin@example.com'); + await expandedFlyout.expectFilterTextEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com OR related.entity: admin@example.com' + ); + await expandedFlyout.expectFilterPreviewEquals( + 0, + 'actor.entity.id: admin@example.com OR target.entity.id: admin@example.com OR related.entity: admin@example.com' + ); + + // Clear filters + await expandedFlyout.clearAllFilters(); + + // Add custom filter + await expandedFlyout.addFilter({ + field: 'actor.entity.id', + operation: 'is', + value: 'admin2@example.com', + }); + await pageObjects.header.waitUntilLoadingHasFinished(); + await expandedFlyout.assertGraphNodesNumber(5); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 0114b6a8ce4dc..67c06d979002f 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -37,5 +37,6 @@ export default function ({ getPageObjects, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./vulnerabilities_grouping')); loadTestFile(require.resolve('./benchmark')); loadTestFile(require.resolve('./alerts_flyout')); + loadTestFile(require.resolve('./events_flyout')); }); } From 34656ae59a43d6ac77ca3ec854c8be446ba5e6a2 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Wed, 20 Nov 2024 18:23:21 +0000 Subject: [PATCH 20/36] Added technical preview tag --- .../left/tabs/visualize_tab.tsx | 19 +++++++++++++++- .../components/graph_preview_container.tsx | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 599cc8e017a2f..9a7024583d6c4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -54,8 +54,25 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [ }, ]; -const graphVisualizationButton = { +const graphVisualizationButton: EuiButtonGroupOptionProps = { id: GRAPH_VISUALIZATION_ID, + iconType: 'beaker', + iconSide: 'right', + toolTipProps: { + title: ( + + ), + }, + toolTipContent: i18n.translate( + 'xpack.securitySolution.flyout.left.visualize.graphVisualizationButton.technicalPreviewTooltip', + { + defaultMessage: + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', + } + ), label: ( { defaultMessage="Graph preview" /> ), + headerContent: ( + + ), iconType: visualizationInFlyoutEnabled ? 'arrowStart' : 'indexMapping', ...(visualizationInFlyoutEnabled && { link: { From c7fb622c2a78aa528fda003901e33d03fc8f038e Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Thu, 21 Nov 2024 13:17:54 +0000 Subject: [PATCH 21/36] post merge fixes --- .../shared/hooks/use_navigate_to_graph_visualization.tsx | 5 +++-- .../api_integration/apis/cloud_security_posture/graph.ts | 4 ++-- x-pack/test/cloud_security_posture_api/routes/graph.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx index 6fed3ce51b22d..e363741866a97 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx @@ -12,6 +12,7 @@ import type { Maybe } from '@kbn/timelines-plugin/common/search_strategy/common' import { useKibana } from '../../../../common/lib/kibana'; import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey } from '../constants/panel_keys'; import { GRAPH_VISUALIZATION_ID } from '../../left/components/graph_visualization'; +import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export interface UseNavigateToGraphVisualizationParams { /** @@ -83,7 +84,7 @@ export const useNavigateToGraphVisualization = ({ const navigateToGraphVisualization = useCallback(() => { if (isFlyoutOpen) { openLeftPanel(left); - telemetry.reportDetailsFlyoutTabClicked({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutTabClicked, { location: scopeId, panel: 'left', tabId: 'visualize', @@ -93,7 +94,7 @@ export const useNavigateToGraphVisualization = ({ right, left, }); - telemetry.reportDetailsFlyoutOpened({ + telemetry.reportEvent(DocumentEventTypes.DetailsFlyoutOpened, { location: scopeId, panel: 'left', }); diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts index 561c14de1b2ff..bcf8ef10f483c 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts @@ -21,14 +21,14 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const postGraph = (agent: Agent, body: GraphRequest, auth?: { user: string; pass: string }) => { - const req = agent + let req = agent .post('/internal/cloud_security_posture/graph') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('kbn-xsrf', 'xxxx'); if (auth) { - req.auth(auth.user, auth.pass); + req = req.auth(auth.user, auth.pass); } return req.send(body); diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index 95625b24fa59a..08adf73839ea2 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -28,14 +28,14 @@ export default function (providerContext: FtrProviderContext) { const cspSecurity = CspSecurityCommonProvider(providerContext); const postGraph = (agent: Agent, body: GraphRequest, auth?: { user: string; pass: string }) => { - const req = agent + let req = agent .post('/internal/cloud_security_posture/graph') .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .set('kbn-xsrf', 'xxxx'); if (auth) { - req.auth(auth.user, auth.pass); + req = req.auth(auth.user, auth.pass); } return req.send(body); From 6969f69c519e903c0903587f3c0d97b3a61f0d5f Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 2 Dec 2024 18:09:09 +0100 Subject: [PATCH 22/36] disables flyout expansion in rule preview and in panel preview --- .../left/components/graph_visualization.tsx | 5 +- .../left/tabs/visualize_tab.tsx | 11 +- .../graph_preview_container.test.tsx | 137 +++++++++++++++++- .../components/graph_preview_container.tsx | 20 ++- .../shared/constants/panel_keys.ts | 2 + ...e_navigate_to_graph_visualization.test.tsx | 11 +- .../use_navigate_to_graph_visualization.tsx | 9 +- 7 files changed, 175 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index b3ab24cf9588f..b401c95181c16 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -27,7 +27,6 @@ import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; -export const GRAPH_VISUALIZATION_ID = 'graph_visualization'; const CONTROLLED_BY_GRAPH_VISUALIZATION_FILTER = 'graph-visualization'; const GraphLazy = React.lazy(() => @@ -223,8 +222,8 @@ export const GraphVisualization: React.FC = memo(() => { showQueryInput: false, isLoading: isFetching, isAutoRefreshDisabled: true, - dateRangeFrom: timeRange.from.split('/')[0], - dateRangeTo: timeRange.to.split('/')[0], + dateRangeFrom: timeRange.from, + dateRangeTo: timeRange.to, query: { query: '', language: 'kuery' }, indexPatterns: [dataView], filters: searchFilters, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 9a7024583d6c4..6bd30d9061d65 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -13,7 +13,10 @@ 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 { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; +import { + DocumentDetailsAnalyzerPanelKey, + VisualizationTabGraphKey, +} from '../../shared/constants/panel_keys'; import { VISUALIZE_TAB_BUTTON_GROUP_TEST_ID, VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID, @@ -28,7 +31,7 @@ import { import { SESSION_VIEW_ID, SessionView } from '../components/session_view'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; -import { GRAPH_VISUALIZATION_ID, GraphVisualization } from '../components/graph_visualization'; +import { GraphVisualization } from '../components/graph_visualization'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ @@ -55,7 +58,7 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [ ]; const graphVisualizationButton: EuiButtonGroupOptionProps = { - id: GRAPH_VISUALIZATION_ID, + id: VisualizationTabGraphKey, iconType: 'beaker', iconSide: 'right', toolTipProps: { @@ -150,7 +153,7 @@ export const VisualizeTab = memo(() => { {activeVisualizationId === SESSION_VIEW_ID && } {activeVisualizationId === ANALYZE_GRAPH_ID && } - {activeVisualizationId === GRAPH_VISUALIZATION_ID && } + {activeVisualizationId === VisualizationTabGraphKey && } ); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index d6cbb7a2f55fc..00b462fe181e9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -22,6 +22,15 @@ import { EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '../../../shared/components/test_ids'; +const mockUseUiSetting = jest.fn().mockReturnValue([true]); +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + useUiSetting$: () => mockUseUiSetting(), + }; +}); + jest.mock('../../shared/hooks/use_graph_preview'); jest.mock('../../shared/hooks/use_fetch_graph_data', () => ({ useFetchGraphData: jest.fn(), @@ -65,6 +74,59 @@ describe('', () => { const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + eventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); + }); + + it('should render component and without link in header in preview panel', async () => { + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: { nodes: [], edges: [] }, + }); + + const timestamp = new Date().toISOString(); + + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: [], + isAuditLog: true, + }); + + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ + ...mockContextValue, + isPreviewMode: true, + }); + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); expect( @@ -98,6 +160,79 @@ describe('', () => { }); }); + it('should render component and without link in header in rule preview', async () => { + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: { nodes: [], edges: [] }, + }); + + const timestamp = new Date().toISOString(); + + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: [], + isAuditLog: true, + }); + + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ + ...mockContextValue, + isPreview: true, + }); + + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + eventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); + }); + + it('should not render when feature is not enabled', () => { + mockUseUiSetting.mockReturnValue([false]); + + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: undefined, + }); + + (useGraphPreview as jest.Mock).mockReturnValue({ + isAuditLog: true, + }); + + const { queryByTestId } = renderGraphPreview(); + + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + }); + it('should not render when graph data is not available', () => { mockUseFetchGraphData.mockReturnValue({ isLoading: false, @@ -112,7 +247,7 @@ describe('', () => { const { queryByTestId } = renderGraphPreview(); expect( - queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) ).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 7f26172ac2e04..8659a29811c50 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -23,13 +23,22 @@ import { ExpandablePanel } from '../../../shared/components/expandable_panel'; * Graph preview under Overview, Visualizations. It shows a graph representation of entities. */ export const GraphPreviewContainer: React.FC = () => { - const { dataAsNestedObject, getFieldsData, eventId, indexName, scopeId } = - useDocumentDetailsContext(); + const { + dataAsNestedObject, + getFieldsData, + eventId, + indexName, + scopeId, + isPreview, + isPreviewMode, + } = useDocumentDetailsContext(); const [visualizationInFlyoutEnabled] = useUiSetting$( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING ); + const allowFlyoutExpansion = !isPreviewMode && !isPreview; + const { navigateToGraphVisualization } = useNavigateToGraphVisualization({ eventId, indexName, @@ -62,7 +71,8 @@ export const GraphPreviewContainer: React.FC = () => { }); return ( - isAuditLog && ( + isAuditLog && + visualizationInFlyoutEnabled && ( { )} /> ), - iconType: visualizationInFlyoutEnabled ? 'arrowStart' : 'indexMapping', - ...(visualizationInFlyoutEnabled && { + iconType: allowFlyoutExpansion ? 'arrowStart' : 'indexMapping', + ...(allowFlyoutExpansion && { link: { callback: navigateToGraphVisualization, tooltip: ( diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts index fa40f1e0e6674..79e603cc99c88 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts @@ -12,3 +12,5 @@ export const DocumentDetailsPreviewPanelKey = 'document-details-preview' as cons export const DocumentDetailsIsolateHostPanelKey = 'document-details-isolate-host' as const; export const DocumentDetailsAlertReasonPanelKey = 'document-details-alert-reason' as const; export const DocumentDetailsAnalyzerPanelKey = 'document-details-analyzer-details' as const; + +export const VisualizationTabGraphKey = 'graph-visualization' as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx index c4c362250a1ac..f44a9a8e2dfa9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx @@ -11,8 +11,11 @@ import { mockFlyoutApi } from '../mocks/mock_flyout_context'; import { useWhichFlyout } from './use_which_flyout'; import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; import { useKibana } from '../../../../common/lib/kibana'; -import { DocumentDetailsRightPanelKey, DocumentDetailsLeftPanelKey } from '../constants/panel_keys'; -import { GRAPH_VISUALIZATION_ID } from '../../left/components/graph_visualization'; +import { + DocumentDetailsRightPanelKey, + DocumentDetailsLeftPanelKey, + VisualizationTabGraphKey, +} from '../constants/panel_keys'; import { useNavigateToGraphVisualization } from './use_navigate_to_graph_visualization'; jest.mock('@kbn/expandable-flyout'); @@ -48,7 +51,7 @@ describe('useNavigateToGraphVisualization', () => { id: DocumentDetailsLeftPanelKey, path: { tab: 'visualize', - subTab: GRAPH_VISUALIZATION_ID, + subTab: VisualizationTabGraphKey, }, params: { id: eventId, @@ -81,7 +84,7 @@ describe('useNavigateToGraphVisualization', () => { id: DocumentDetailsLeftPanelKey, path: { tab: 'visualize', - subTab: GRAPH_VISUALIZATION_ID, + subTab: VisualizationTabGraphKey, }, params: { id: eventId, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx index e363741866a97..634063a54efff 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx @@ -10,8 +10,11 @@ import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { Maybe } from '@kbn/timelines-plugin/common/search_strategy/common'; import { useKibana } from '../../../../common/lib/kibana'; -import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey } from '../constants/panel_keys'; -import { GRAPH_VISUALIZATION_ID } from '../../left/components/graph_visualization'; +import { + DocumentDetailsLeftPanelKey, + DocumentDetailsRightPanelKey, + VisualizationTabGraphKey, +} from '../constants/panel_keys'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; export interface UseNavigateToGraphVisualizationParams { @@ -75,7 +78,7 @@ export const useNavigateToGraphVisualization = ({ }, path: { tab: 'visualize', - subTab: GRAPH_VISUALIZATION_ID, + subTab: VisualizationTabGraphKey, }, }), [eventId, indexName, scopeId] From 21d04e2f945475c02c54b57f2fa12a559c62fa6d Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 2 Dec 2024 18:12:17 +0100 Subject: [PATCH 23/36] refactoring --- .../components/graph_node_expand_popover.tsx | 21 ++++++++++--------- .../left/components/test_ids.ts | 2 ++ .../use_graph_node_expand_popover.tsx | 9 +++----- .../page_objects/expanded_flyout.ts | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx index 7389493ef06df..648d27e42103b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx @@ -11,6 +11,7 @@ import { EuiListGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ExpandPopoverListItem } from '@kbn/cloud-security-posture-graph/src/components/styles'; import { + GRAPH_NODE_EXPAND_POPOVER_TEST_ID, GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID, GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID, GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID, @@ -41,18 +42,9 @@ export const GraphNodeExpandPopover: React.FC = mem isOpen={isOpen} anchorElement={anchorElement} closePopover={closePopover} - data-test-subj="graphNodeExpandPopover" + data-test-subj={GRAPH_NODE_EXPAND_POPOVER_TEST_ID} > - = mem onClick={onShowActionsOnEntityClick} data-test-subj={GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID} /> + ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index 4bb2b7247d122..218a2602be566 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -12,6 +12,8 @@ import { PREFIX } from '../../../shared/test_ids'; export const ANALYZER_GRAPH_TEST_ID = `${PREFIX}AnalyzerGraph` as const; export const SESSION_VIEW_TEST_ID = `${PREFIX}SessionView` as const; export const GRAPH_VISUALIZATION_TEST_ID = `${PREFIX}GraphVisualization` as const; +export const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = + `${GRAPH_VISUALIZATION_TEST_ID}GraphNodeExpandPopover` as const; export const GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID = `${GRAPH_VISUALIZATION_TEST_ID}ExploreRelatedEntities` as const; export const GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx index e06ed1bca705a..8b836249e5c26 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx @@ -45,13 +45,10 @@ export const useGraphNodeExpandPopover = ({ const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( (e, node, unToggleCallback) => { - if (selectedNode.current?.id === node.id) { - // If the same node is clicked again, close the popover - closePopoverHandler(); - } else { - // Close the current popover if open - closePopoverHandler(); + // Close the current popover if open + closePopoverHandler(); + if (selectedNode.current?.id !== node.id) { // Set the pending open state setPendingOpen({ node, el: e.currentTarget, unToggleCallback }); } diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts b/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts index 062425f939477..a1f5f2b1ba4c0 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/expanded_flyout.ts @@ -13,7 +13,7 @@ import { FtrService } from '../../functional/ftr_provider_context'; const GRAPH_PREVIEW_TITLE_LINK_TEST_ID = 'securitySolutionFlyoutGraphPreviewTitleLink'; const GRAPH_VISUALIZATION_TEST_ID = 'securitySolutionFlyoutGraphVisualization'; const NODE_EXPAND_BUTTON_TEST_ID = 'nodeExpandButton'; -const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = 'graphNodeExpandPopover'; +const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}GraphNodeExpandPopover`; const GRAPH_NODE_POPOVER_EXPLORE_RELATED_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}ExploreRelatedEntities`; const GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}ShowActionsByEntity`; const GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID = `${GRAPH_VISUALIZATION_TEST_ID}ShowActionsOnEntity`; From fef5ea3189b25e9bdb3e487724e25659100719f3 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 9 Dec 2024 15:23:01 +0100 Subject: [PATCH 24/36] refactoring --- .../document_details/shared/hooks/use_navigate_to_analyzer.tsx | 2 +- .../shared/hooks/use_navigate_to_graph_visualization.tsx | 2 +- .../shared/hooks/use_navigate_to_session_view.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx index a4539ed7e6415..2137ce83527a8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_analyzer.tsx @@ -49,7 +49,7 @@ export interface UseNavigateToAnalyzerResult { } /** - * Hook that returns the a callback to navigate to the analyzer in the flyout + * Hook that returns a callback to navigate to the analyzer in the flyout */ export const useNavigateToAnalyzer = ({ isFlyoutOpen, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx index 634063a54efff..51e74a54250ed 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx @@ -45,7 +45,7 @@ export interface UseNavigateToGraphVisualizationResult { } /** - * Hook that returns the a callback to navigate to the graph visualization in the flyout + * Hook that returns a callback to navigate to the graph visualization in the flyout */ export const useNavigateToGraphVisualization = ({ isFlyoutOpen, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx index f0b2733998c97..d98ce5f489e38 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_session_view.tsx @@ -42,7 +42,7 @@ export interface UseNavigateToSessionViewResult { } /** - * Hook that returns the a callback to navigate to session view in the flyout + * Hook that returns a callback to navigate to session view in the flyout */ export const useNavigateToSessionView = ({ isFlyoutOpen, From 6b81c5dfb547168994d6e869339a5ed12fc19667 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 9 Dec 2024 15:23:15 +0100 Subject: [PATCH 25/36] fix tests --- .../hooks/use_fetch_graph_data.test.tsx | 43 ++++++++++++++++--- .../test_suites/security/config.ts | 2 + 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx index c22ec0caa82c5..e494ff0957ecb 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx @@ -13,15 +13,22 @@ const mockUseQuery = jest.fn(); jest.mock('@tanstack/react-query', () => { return { useQuery: (...args: unknown[]) => mockUseQuery(...args), + useQueryClient: jest.fn(), }; }); +const defaultOptions = { + enabled: true, + refetchOnWindowFocus: true, + keepPreviousData: false, +}; + describe('useFetchGraphData', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('Should pass default options when options are not provided', () => { + it('should pass default options when options are not provided', () => { renderHook(() => { return useFetchGraphData({ req: { @@ -36,12 +43,11 @@ describe('useFetchGraphData', () => { expect(mockUseQuery.mock.calls).toHaveLength(1); expect(mockUseQuery.mock.calls[0][2]).toEqual({ - enabled: true, - refetchOnWindowFocus: true, + ...defaultOptions, }); }); - it('Should should not be enabled when enabled set to false', () => { + it('should not be enabled when enabled set to false', () => { renderHook(() => { return useFetchGraphData({ req: { @@ -59,12 +65,12 @@ describe('useFetchGraphData', () => { expect(mockUseQuery.mock.calls).toHaveLength(1); expect(mockUseQuery.mock.calls[0][2]).toEqual({ + ...defaultOptions, enabled: false, - refetchOnWindowFocus: true, }); }); - it('Should should not be refetchOnWindowFocus when refetchOnWindowFocus set to false', () => { + it('should not be refetchOnWindowFocus when refetchOnWindowFocus set to false', () => { renderHook(() => { return useFetchGraphData({ req: { @@ -82,8 +88,31 @@ describe('useFetchGraphData', () => { expect(mockUseQuery.mock.calls).toHaveLength(1); expect(mockUseQuery.mock.calls[0][2]).toEqual({ - enabled: true, + ...defaultOptions, refetchOnWindowFocus: false, }); }); + + it('should keepPreviousData when keepPreviousData set to true', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + options: { + keepPreviousData: true, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + ...defaultOptions, + keepPreviousData: true, + }); + }); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.ts index d5d816dfcdf17..a75c3e4186a02 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -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}`, + + `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=true`, // Enables /graph API ], }); From 4b1512e6b11129a7051c37d8b4b0033db36bc41e Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 9 Dec 2024 19:28:28 +0100 Subject: [PATCH 26/36] Refactored graph visualization component --- .../kbn-cloud-security-posture/graph/index.ts | 1 + .../graph/src/common/constants.ts | 8 + .../graph_investigation.tsx | 263 ++++++++++++++++++ .../graph_node_expand_popover.tsx | 6 +- .../use_graph_node_expand_popover.tsx | 23 +- .../graph/src/components/index.ts | 1 + .../graph/src/components/test_ids.ts | 18 ++ .../graph/src/hooks/index.ts | 8 + .../src}/hooks/use_fetch_graph_data.test.tsx | 0 .../graph/src}/hooks/use_fetch_graph_data.ts | 14 +- .../graph/tsconfig.json | 4 +- .../security_solution/common/constants.ts | 2 - .../left/components/graph_visualization.tsx | 242 ++-------------- .../left/components/test_ids.ts | 8 - .../components/graph_preview_container.tsx | 2 +- 15 files changed, 340 insertions(+), 260 deletions(-) create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx rename x-pack/{plugins/security_solution/public/flyout/document_details/left/components => packages/kbn-cloud-security-posture/graph/src/components/graph_investigation}/graph_node_expand_popover.tsx (93%) rename x-pack/{plugins/security_solution/public/flyout/document_details/left/components => packages/kbn-cloud-security-posture/graph/src/components/graph_investigation}/use_graph_node_expand_popover.tsx (83%) create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/index.ts rename x-pack/{plugins/security_solution/public/flyout/document_details/shared => packages/kbn-cloud-security-posture/graph/src}/hooks/use_fetch_graph_data.test.tsx (100%) rename x-pack/{plugins/security_solution/public/flyout/document_details/shared => packages/kbn-cloud-security-posture/graph/src}/hooks/use_fetch_graph_data.ts (90%) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/index.ts b/x-pack/packages/kbn-cloud-security-posture/graph/index.ts index c50969cfd6402..45575316b29d9 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/index.ts @@ -6,3 +6,4 @@ */ export * from './src/components'; +export { useFetchGraphData } from './src/hooks'; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts new file mode 100644 index 0000000000000..4e02c83bf3a54 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export const EVENT_GRAPH_VISUALIZATION_API = '/internal/cloud_security_posture/graph' as const; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx new file mode 100644 index 0000000000000..e8149141ac72d --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx @@ -0,0 +1,263 @@ +/* + * 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, { memo, useEffect, useMemo, useState } from 'react'; +import { SearchBar } from '@kbn/unified-search-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { + BooleanRelation, + buildEsQuery, + isCombinedFilter, + buildCombinedFilter, + isPhraseFilter, +} from '@kbn/es-query'; +import type { Filter, Query, TimeRange, BoolQuery, PhraseFilter } from '@kbn/es-query'; +import { css } from '@emotion/react'; +import { getEsQueryConfig } from '@kbn/data-service'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Graph, type NodeViewModel } from '../../..'; +import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; +import { useFetchGraphData } from '../../hooks/use_fetch_graph_data'; + +const CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER = 'graph-investigation'; + +const useTimeRange = (timestamp: string) => { + const [timeRange, setTimeRange] = useState({ + from: `${timestamp}||-30m`, + to: `${timestamp}||+30m`, + }); + + const setPartialTimeRange = (newTimeRange: Partial) => { + setTimeRange((currTimeRange) => ({ ...currTimeRange, ...newTimeRange })); + }; + + return { timeRange, setTimeRange, setPartialTimeRange }; +}; + +const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: BoolQuery }) => { + const { data, refresh, isFetching } = useFetchGraphData({ + req: { + query: { + eventIds, + esQuery: filter, + start: timeRange.from, + end: timeRange.to, + }, + }, + options: { + refetchOnWindowFocus: false, + keepPreviousData: true, + }, + }); + + return { data, refresh, isFetching }; +}; + +const buildPhraseFilter = (field: string, value: string, dataViewId?: string): PhraseFilter => ({ + meta: { + key: field, + index: dataViewId, + negate: false, + disabled: false, + type: 'phrase', + field, + controlledBy: CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER, + params: { + query: value, + }, + }, + query: { + match_phrase: { + [field]: value, + }, + }, +}); + +const addFilter = (dataViewId: string, prev: Filter[], key: string, value: string) => { + const [firstFilter, ...otherFilters] = prev; + + if (isCombinedFilter(firstFilter) && firstFilter?.meta?.relation === BooleanRelation.OR) { + return [ + { + ...firstFilter, + meta: { + ...firstFilter.meta, + params: [ + ...(Array.isArray(firstFilter.meta.params) ? firstFilter.meta.params : []), + buildPhraseFilter(key, value), + ], + }, + }, + ...otherFilters, + ]; + } else if (isPhraseFilter(firstFilter)) { + return [ + buildCombinedFilter(BooleanRelation.OR, [firstFilter, buildPhraseFilter(key, value)], { + id: dataViewId, + }), + ...otherFilters, + ]; + } else { + return [buildPhraseFilter(key, value, dataViewId), ...prev]; + } +}; + +const useGraphPopovers = ( + dataViewId: string, + setSearchFilters: React.Dispatch> +) => { + const nodeExpandPopover = useGraphNodeExpandPopover({ + onExploreRelatedEntitiesClick: (node) => { + setSearchFilters((prev) => addFilter(dataViewId, prev, 'related.entity', node.id)); + }, + onShowActionsByEntityClick: (node) => { + setSearchFilters((prev) => addFilter(dataViewId, prev, 'actor.entity.id', node.id)); + }, + onShowActionsOnEntityClick: (node) => { + setSearchFilters((prev) => addFilter(dataViewId, prev, 'target.entity.id', node.id)); + }, + }); + + const popovers = [nodeExpandPopover]; + const popoverOpenWrapper = (cb: Function, ...args: unknown[]) => { + popovers.forEach(({ actions: { closePopover } }) => { + closePopover(); + }); + cb(...args); + }; + + return { nodeExpandPopover, popoverOpenWrapper }; +}; + +const useGraphNodes = ( + nodes: NodeViewModel[], + expandButtonClickHandler: (...args: unknown[]) => void +) => { + return useMemo(() => { + return nodes.map((node) => { + const nodeHandlers = + node.shape !== 'label' && node.shape !== 'group' + ? { + expandButtonClick: expandButtonClickHandler, + } + : undefined; + return { ...node, ...nodeHandlers }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodes]); +}; + +interface GraphInvestigationProps { + dataView: DataView; + eventIds: string[]; + timestamp: string | null; +} + +/** + * Graph investigation view allows the user to expand nodes and view related entities. + */ +export const GraphInvestigation: React.FC = memo( + ({ dataView, eventIds, timestamp }: GraphInvestigationProps) => { + const [searchFilters, setSearchFilters] = useState(() => []); + const { timeRange, setTimeRange } = useTimeRange(timestamp ?? new Date().toISOString()); + + const { + services: { uiSettings }, + } = useKibana(); + const [query, setQuery] = useState<{ bool: BoolQuery }>( + buildEsQuery( + dataView, + [], + [...searchFilters], + getEsQueryConfig(uiSettings as Parameters[0]) + ) + ); + + useEffect(() => { + setQuery( + buildEsQuery( + dataView, + [], + [...searchFilters], + getEsQueryConfig(uiSettings as Parameters[0]) + ) + ); + }, [searchFilters, dataView, uiSettings]); + + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers( + dataView?.id ?? '', + setSearchFilters + ); + const expandButtonClickHandler = (...args: unknown[]) => + popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); + const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); + const { data, refresh, isFetching } = useGraphData(eventIds, timeRange, query); + const nodes = useGraphNodes(data?.nodes ?? [], expandButtonClickHandler); + + return ( + <> + + {dataView && ( + + + {...{ + appName: 'graph-investigation', + intl: null, + showFilterBar: true, + showDatePicker: true, + showAutoRefreshOnly: false, + showSaveQuery: false, + showQueryInput: false, + isLoading: isFetching, + isAutoRefreshDisabled: true, + dateRangeFrom: timeRange.from, + dateRangeTo: timeRange.to, + query: { query: '', language: 'kuery' }, + indexPatterns: [dataView], + filters: searchFilters, + submitButtonStyle: 'iconOnly', + onFiltersUpdated: (newFilters) => { + setSearchFilters(newFilters); + }, + onQuerySubmit: (payload, isUpdate) => { + if (isUpdate) { + setTimeRange({ ...payload.dateRange }); + } else { + refresh(); + } + }, + }} + /> + + )} + + + + + + + ); + } +); + +GraphInvestigation.displayName = 'GraphInvestigation'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx similarity index 93% rename from x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx rename to x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx index 648d27e42103b..2a24a00bd28fa 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_node_expand_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx @@ -6,16 +6,16 @@ */ import React, { memo } from 'react'; -import { GraphPopover } from '@kbn/cloud-security-posture-graph'; import { EuiListGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ExpandPopoverListItem } from '@kbn/cloud-security-posture-graph/src/components/styles'; +import { ExpandPopoverListItem } from '../styles'; +import { GraphPopover } from '../../..'; import { GRAPH_NODE_EXPAND_POPOVER_TEST_ID, GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID, GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID, GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID, -} from './test_ids'; +} from '../test_ids'; interface GraphNodeExpandPopoverProps { isOpen: boolean; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx rename to x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx index 8b836249e5c26..7178f5f9dbca7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/use_graph_node_expand_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx @@ -5,13 +5,9 @@ * 2.0. */ -import { useGraphPopover } from '@kbn/cloud-security-posture-graph'; -import type { - ExpandButtonClickCallback, - NodeProps, -} from '@kbn/cloud-security-posture-graph/src/components/types'; -import type { PopoverActions } from '@kbn/cloud-security-posture-graph/src/components/graph/use_graph_popover'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useGraphPopover } from '../../..'; +import type { ExpandButtonClickCallback, NodeProps } from '../types'; import { GraphNodeExpandPopover } from './graph_node_expand_popover'; interface UseGraphNodeExpandPopoverArgs { @@ -91,22 +87,17 @@ export const useGraphNodeExpandPopover = ({ PopoverComponent.displayName = GraphNodeExpandPopover.displayName; - const actionsWithClose: PopoverActions = useMemo( - () => ({ - ...actions, - closePopover: closePopoverHandler, - }), - [actions, closePopoverHandler] - ); - return useMemo( () => ({ onNodeExpandButtonClick, PopoverComponent, id, - actions: actionsWithClose, + actions: { + ...actions, + closePopover: closePopoverHandler, + }, state, }), - [PopoverComponent, actionsWithClose, id, onNodeExpandButtonClick, state] + [PopoverComponent, actions, closePopoverHandler, id, onNodeExpandButtonClick, state] ); }; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts index 2b050aa55429f..d3cd397764e60 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/index.ts @@ -6,6 +6,7 @@ */ export { Graph } from './graph/graph'; +export { GraphInvestigation } from './graph_investigation/graph_investigation'; export { GraphPopover } from './graph/graph_popover'; export { useGraphPopover } from './graph/use_graph_popover'; export type { GraphProps } from './graph/graph'; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts new file mode 100644 index 0000000000000..2fed9486840b2 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export const PREFIX = 'cloudSecurityGraph' as const; + +export const GRAPH_INVESTIGATION_TEST_ID = `${PREFIX}GraphInvestigation` as const; +export const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = + `${GRAPH_INVESTIGATION_TEST_ID}GraphNodeExpandPopover` as const; +export const GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID = + `${GRAPH_INVESTIGATION_TEST_ID}ExploreRelatedEntities` as const; +export const GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID = + `${GRAPH_INVESTIGATION_TEST_ID}ShowActionsByEntity` as const; +export const GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID = + `${GRAPH_INVESTIGATION_TEST_ID}ShowActionsOnEntity` as const; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/index.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/index.ts new file mode 100644 index 0000000000000..6d75dc8beefee --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { useFetchGraphData } from './use_fetch_graph_data'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.test.tsx rename to x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.test.tsx diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts similarity index 90% rename from x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.ts rename to x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts index 964f5824f9074..74cca4693e801 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_graph_data.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/hooks/use_fetch_graph_data.ts @@ -5,14 +5,14 @@ * 2.0. */ +import { useMemo } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { GraphRequest, GraphResponse, } from '@kbn/cloud-security-posture-common/types/graph/latest'; -import { useMemo } from 'react'; -import { EVENT_GRAPH_VISUALIZATION_API } from '../../../../../common/constants'; -import { useHttp } from '../../../../common/lib/kibana'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { EVENT_GRAPH_VISUALIZATION_API } from '../common/constants'; /** * Interface for the input parameters of the useFetchGraphData hook. @@ -82,7 +82,9 @@ export const useFetchGraphData = ({ }: UseFetchGraphDataParams): UseFetchGraphDataResult => { const queryClient = useQueryClient(); const { esQuery, eventIds, start, end } = req.query; - const http = useHttp(); + const { + services: { http }, + } = useKibana(); const QUERY_KEY = useMemo( () => ['useFetchGraphData', eventIds, start, end, esQuery], [end, esQuery, eventIds, start] @@ -91,6 +93,10 @@ export const useFetchGraphData = ({ const { isLoading, isError, data, isFetching } = useQuery( QUERY_KEY, () => { + if (!http) { + return Promise.reject(new Error('Http service is not available')); + } + return http.post(EVENT_GRAPH_VISUALIZATION_API, { version: '1', body: JSON.stringify(req), diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json index d0056e29e6784..ba289c0780f80 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json @@ -12,7 +12,9 @@ ], "kbn_references": [ "@kbn/cloud-security-posture-common", - "@kbn/utility-types", + "@kbn/data-views-plugin", + "@kbn/kibana-react-plugin", "@kbn/ui-theme", + "@kbn/utility-types", ] } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 265af5a47e1fe..3818813d94a72 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -282,8 +282,6 @@ export const PINNED_EVENT_URL = '/api/pinned_event' as const; export const SOURCERER_API_URL = '/internal/security_solution/sourcerer' as const; export const RISK_SCORE_INDEX_STATUS_API_URL = '/internal/risk_score/index_status' as const; -export const EVENT_GRAPH_VISUALIZATION_API = '/internal/cloud_security_posture/graph' as const; - /** * Default signals index key for kibana.dev.yml */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index b401c95181c16..308877fb3980c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -5,160 +5,21 @@ * 2.0. */ -import React, { memo, useEffect, useMemo, useState } from 'react'; -import { SearchBar } from '@kbn/unified-search-plugin/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { NodeViewModel } from '@kbn/cloud-security-posture-graph'; -import { - BooleanRelation, - buildEsQuery, - isCombinedFilter, - buildCombinedFilter, - isPhraseFilter, -} from '@kbn/es-query'; -import type { Filter, Query, TimeRange, BoolQuery, PhraseFilter } from '@kbn/es-query'; +import React, { memo } from 'react'; import { css } from '@emotion/css'; -import { getEsQueryConfig } from '@kbn/data-service'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { useGetScopedSourcererDataView } from '../../../../sourcerer/components/use_get_sourcerer_data_view'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_VISUALIZATION_TEST_ID } from './test_ids'; -import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; -const CONTROLLED_BY_GRAPH_VISUALIZATION_FILTER = 'graph-visualization'; - -const GraphLazy = React.lazy(() => - import('@kbn/cloud-security-posture-graph').then((module) => ({ default: module.Graph })) +const GraphInvestigationLazy = React.lazy(() => + import('@kbn/cloud-security-posture-graph').then((module) => ({ + default: module.GraphInvestigation, + })) ); -const useTimeRange = (timestamp: string) => { - const [timeRange, setTimeRange] = useState({ - from: `${timestamp}||-30m`, - to: `${timestamp}||+30m`, - }); - - const setPartialTimeRange = (newTimeRange: Partial) => { - setTimeRange((currTimeRange) => ({ ...currTimeRange, ...newTimeRange })); - }; - - return { timeRange, setTimeRange, setPartialTimeRange }; -}; - -const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: BoolQuery }) => { - const { data, refresh, isFetching } = useFetchGraphData({ - req: { - query: { - eventIds, - esQuery: filter, - start: timeRange.from, - end: timeRange.to, - }, - }, - options: { - refetchOnWindowFocus: false, - keepPreviousData: true, - }, - }); - - return { data, refresh, isFetching }; -}; - -const buildPhraseFilter = (field: string, value: string, dataViewId?: string): PhraseFilter => ({ - meta: { - key: field, - index: dataViewId, - negate: false, - disabled: false, - type: 'phrase', - field, - controlledBy: CONTROLLED_BY_GRAPH_VISUALIZATION_FILTER, - params: { - query: value, - }, - }, - query: { - match_phrase: { - [field]: value, - }, - }, -}); - -const addFilter = (dataViewId: string, prev: Filter[], key: string, value: string) => { - const [firstFilter, ...otherFilters] = prev; - - if (isCombinedFilter(firstFilter) && firstFilter?.meta?.relation === BooleanRelation.OR) { - return [ - { - ...firstFilter, - meta: { - ...firstFilter.meta, - params: [ - ...(Array.isArray(firstFilter.meta.params) ? firstFilter.meta.params : []), - buildPhraseFilter(key, value), - ], - }, - }, - ...otherFilters, - ]; - } else if (isPhraseFilter(firstFilter)) { - return [ - buildCombinedFilter(BooleanRelation.OR, [firstFilter, buildPhraseFilter(key, value)], { - id: dataViewId, - }), - ...otherFilters, - ]; - } else { - return [buildPhraseFilter(key, value, dataViewId), ...prev]; - } -}; - -const useGraphPopovers = ( - dataViewId: string, - setSearchFilters: React.Dispatch> -) => { - const nodeExpandPopover = useGraphNodeExpandPopover({ - onExploreRelatedEntitiesClick: (node) => { - setSearchFilters((prev) => addFilter(dataViewId, prev, 'related.entity', node.id)); - }, - onShowActionsByEntityClick: (node) => { - setSearchFilters((prev) => addFilter(dataViewId, prev, 'actor.entity.id', node.id)); - }, - onShowActionsOnEntityClick: (node) => { - setSearchFilters((prev) => addFilter(dataViewId, prev, 'target.entity.id', node.id)); - }, - }); - - const popovers = [nodeExpandPopover]; - const popoverOpenWrapper = (cb: Function, ...args: unknown[]) => { - popovers.forEach(({ actions: { closePopover } }) => { - closePopover(); - }); - cb(...args); - }; - - return { nodeExpandPopover, popoverOpenWrapper }; -}; - -const useGraphNodes = ( - nodes: NodeViewModel[], - expandButtonClickHandler: (...args: unknown[]) => void -) => { - return useMemo(() => { - return nodes.map((node) => { - const nodeHandlers = - node.shape !== 'label' && node.shape !== 'group' - ? { - expandButtonClick: expandButtonClickHandler, - } - : undefined; - return { ...node, ...nodeHandlers }; - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nodes]); -}; - /** * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab */ @@ -166,95 +27,26 @@ export const GraphVisualization: React.FC = memo(() => { const dataView = useGetScopedSourcererDataView({ sourcererScope: SourcererScopeName.default, }); - const [searchFilters, setSearchFilters] = useState(() => []); const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); const { eventIds, timestamp } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); - const { timeRange, setTimeRange } = useTimeRange(timestamp ?? new Date().toISOString()); - - const { - services: { uiSettings }, - } = useKibana(); - const [query, setQuery] = useState<{ bool: BoolQuery }>( - buildEsQuery( - dataView, - [], - [...searchFilters], - getEsQueryConfig(uiSettings as Parameters[0]) - ) - ); - - useEffect(() => { - setQuery( - buildEsQuery( - dataView, - [], - [...searchFilters], - getEsQueryConfig(uiSettings as Parameters[0]) - ) - ); - }, [searchFilters, dataView, uiSettings]); - - const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers( - dataView?.id ?? '', - setSearchFilters - ); - const expandButtonClickHandler = (...args: unknown[]) => - popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); - const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); - const { data, refresh, isFetching } = useGraphData(eventIds, timeRange, query); - const nodes = useGraphNodes(data?.nodes ?? [], expandButtonClickHandler); - return ( -
+
{dataView && ( - - {...{ - appName: 'graph-visualization', - intl: null, - showFilterBar: true, - showDatePicker: true, - showAutoRefreshOnly: false, - showSaveQuery: false, - showQueryInput: false, - isLoading: isFetching, - isAutoRefreshDisabled: true, - dateRangeFrom: timeRange.from, - dateRangeTo: timeRange.to, - query: { query: '', language: 'kuery' }, - indexPatterns: [dataView], - filters: searchFilters, - submitButtonStyle: 'iconOnly', - onFiltersUpdated: (newFilters) => { - setSearchFilters(newFilters); - }, - onQuerySubmit: (payload, isUpdate) => { - if (isUpdate) { - setTimeRange({ ...payload.dateRange }); - } else { - refresh(); - } - }, - }} - /> + }> + + )} - - - -
); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index 218a2602be566..6979fa9cfa053 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -12,14 +12,6 @@ import { PREFIX } from '../../../shared/test_ids'; export const ANALYZER_GRAPH_TEST_ID = `${PREFIX}AnalyzerGraph` as const; export const SESSION_VIEW_TEST_ID = `${PREFIX}SessionView` as const; export const GRAPH_VISUALIZATION_TEST_ID = `${PREFIX}GraphVisualization` as const; -export const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = - `${GRAPH_VISUALIZATION_TEST_ID}GraphNodeExpandPopover` as const; -export const GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID = - `${GRAPH_VISUALIZATION_TEST_ID}ExploreRelatedEntities` as const; -export const GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID = - `${GRAPH_VISUALIZATION_TEST_ID}ShowActionsByEntity` as const; -export const GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID = - `${GRAPH_VISUALIZATION_TEST_ID}ShowActionsOnEntity` as const; /* Insights tab */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 8659a29811c50..82888d56edc79 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -10,11 +10,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { EuiBetaBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useFetchGraphData } from '@kbn/cloud-security-posture-graph/src/hooks'; import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; import { GraphPreview } from './graph_preview'; -import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useNavigateToGraphVisualization } from '../../shared/hooks/use_navigate_to_graph_visualization'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; From b13bc2aaef79edb66bac9b00a3fa2a2a2f24abe4 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:38:06 +0000 Subject: [PATCH 27/36] [CI] Auto-commit changed files from 'node scripts/notice' --- .../packages/kbn-cloud-security-posture/graph/tsconfig.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json index ba289c0780f80..e56b9aabf16a9 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/graph/tsconfig.json @@ -16,5 +16,9 @@ "@kbn/kibana-react-plugin", "@kbn/ui-theme", "@kbn/utility-types", + "@kbn/unified-search-plugin", + "@kbn/es-query", + "@kbn/data-service", + "@kbn/i18n", ] } From 2f0ea4aed9e07a62a4c2f682360cae5a619a3239 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 9 Dec 2024 19:54:55 +0100 Subject: [PATCH 28/36] tests fixes --- .../graph_investigation.tsx | 2 ++ .../page_objects/expanded_flyout.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx index e8149141ac72d..af5715f1c97da 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx @@ -23,6 +23,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Graph, type NodeViewModel } from '../../..'; import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; import { useFetchGraphData } from '../../hooks/use_fetch_graph_data'; +import { GRAPH_INVESTIGATION_TEST_ID } from '../test_ids'; const CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER = 'graph-investigation'; @@ -202,6 +203,7 @@ export const GraphInvestigation: React.FC = memo( return ( <> [0]; export class ExpandedFlyout extends FtrService { @@ -29,12 +29,12 @@ export class ExpandedFlyout extends FtrService { } async waitGraphIsLoaded(): Promise { - await this.testSubjects.existOrFail(GRAPH_VISUALIZATION_TEST_ID, { timeout: 10000 }); + await this.testSubjects.existOrFail(GRAPH_INVESTIGATION_TEST_ID, { timeout: 10000 }); } async assertGraphNodesNumber(expected: number): Promise { await this.waitGraphIsLoaded(); - const graph = await this.testSubjects.find(GRAPH_VISUALIZATION_TEST_ID); + const graph = await this.testSubjects.find(GRAPH_INVESTIGATION_TEST_ID); await graph.scrollIntoView(); const nodes = await graph.findAllByCssSelector('.react-flow__nodes .react-flow__node'); expect(nodes.length).to.be(expected); @@ -42,7 +42,7 @@ export class ExpandedFlyout extends FtrService { async selectNode(nodeId: string): Promise { await this.waitGraphIsLoaded(); - const graph = await this.testSubjects.find(GRAPH_VISUALIZATION_TEST_ID); + const graph = await this.testSubjects.find(GRAPH_INVESTIGATION_TEST_ID); await graph.scrollIntoView(); const nodes = await graph.findAllByCssSelector( `.react-flow__nodes .react-flow__node[data-id="${nodeId}"]` @@ -97,13 +97,13 @@ export class ExpandedFlyout extends FtrService { } async clearAllFilters(): Promise { - await this.testSubjects.click(`${GRAPH_VISUALIZATION_TEST_ID} > showQueryBarMenu`); + await this.testSubjects.click(`${GRAPH_INVESTIGATION_TEST_ID} > showQueryBarMenu`); await this.testSubjects.click('filter-sets-removeAllFilters'); await this.pageObjects.header.waitUntilLoadingHasFinished(); } async addFilter(filter: Filter): Promise { - await this.testSubjects.click(`${GRAPH_VISUALIZATION_TEST_ID} > addFilter`); + await this.testSubjects.click(`${GRAPH_INVESTIGATION_TEST_ID} > addFilter`); await this.filterBar.createFilter(filter); await this.testSubjects.scrollIntoView('saveFilter'); await this.testSubjects.clickWhenNotDisabled('saveFilter'); From 20be49b1183df54de4a99aba1b5ea4af7ffe228c Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 9 Dec 2024 20:06:11 +0100 Subject: [PATCH 29/36] fix i18n --- .../graph_investigation/graph_node_expand_popover.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx index 2a24a00bd28fa..ba3234b4b8883 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx @@ -48,7 +48,7 @@ export const GraphNodeExpandPopover: React.FC = mem = mem = mem Date: Mon, 9 Dec 2024 20:28:03 +0100 Subject: [PATCH 30/36] quick check fix --- .../graph_node_expand_popover.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx index ba3234b4b8883..45cd5a167ffa5 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx @@ -47,28 +47,25 @@ export const GraphNodeExpandPopover: React.FC = mem From bcbe7f6c341bb9c99e4ac17dc7d416c9bbeb880e Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Mon, 9 Dec 2024 20:34:59 +0100 Subject: [PATCH 31/36] linting fixes --- .../right/components/graph_preview_container.test.tsx | 4 ++-- .../right/components/visualizations_section.test.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index 00b462fe181e9..6567f2b8412b6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -6,6 +6,7 @@ */ import { render } from '@testing-library/react'; +import { useFetchGraphData } from '@kbn/cloud-security-posture-graph/src/hooks'; import { TestProviders } from '../../../../common/mock'; import React from 'react'; import { DocumentDetailsContext } from '../../shared/context'; @@ -13,7 +14,6 @@ import { mockContextValue } from '../../shared/mocks/mock_context'; import { GraphPreviewContainer } from './graph_preview_container'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID, EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID, @@ -32,7 +32,7 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { }); jest.mock('../../shared/hooks/use_graph_preview'); -jest.mock('../../shared/hooks/use_fetch_graph_data', () => ({ +jest.mock('@kbn/cloud-security-posture-graph/src/hooks', () => ({ useFetchGraphData: jest.fn(), })); const mockUseFetchGraphData = useFetchGraphData as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index fef1d72c0df19..216851e0202ed 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { render } from '@testing-library/react'; +import { useFetchGraphData } from '@kbn/cloud-security-posture-graph/src/hooks'; import { ANALYZER_PREVIEW_TEST_ID, SESSION_PREVIEW_TEST_ID, @@ -26,7 +27,6 @@ import { useExpandSection } from '../hooks/use_expand_section'; import { useInvestigateInTimeline } from '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; -import { useFetchGraphData } from '../../shared/hooks/use_fetch_graph_data'; jest.mock('../hooks/use_expand_section'); jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ @@ -65,7 +65,7 @@ jest.mock('../../shared/hooks/use_graph_preview'); const mockUseGraphPreview = useGraphPreview as jest.Mock; -jest.mock('../../shared/hooks/use_fetch_graph_data', () => ({ +jest.mock('@kbn/cloud-security-posture-graph/src/hooks', () => ({ useFetchGraphData: jest.fn(), })); From 1f0bcec987847c94c258b48878c22220fbf08de2 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 10 Dec 2024 20:48:37 +0100 Subject: [PATCH 32/36] added back graph vis in flyout experimental feature --- .../translations/translations/fr-FR.json | 1 + .../translations/translations/ja-JP.json | 1 + .../translations/translations/zh-CN.json | 1 + .../server/routes/graph/route.ts | 9 ----- .../common/experimental_features.ts | 5 +++ .../left/tabs/visualize_tab.tsx | 10 ++++-- .../graph_preview_container.test.tsx | 35 ++++++++++++++++++- .../components/graph_preview_container.tsx | 11 ++++-- .../visualizations_section.test.tsx | 29 +++++++++++++++ .../components/visualizations_section.tsx | 13 +++++-- .../shared/constants/experimental_features.ts | 10 ++++++ .../security_solution/server/ui_settings.ts | 2 +- .../apis/cloud_security_posture/config.ts | 1 - .../apis/cloud_security_posture/graph.ts | 3 +- .../test/cloud_security_posture_api/config.ts | 1 - .../config.ts | 3 ++ .../test_suites/security/config.ts | 2 -- 17 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/experimental_features.ts diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 985a8120d1721..81298783701e2 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -42544,6 +42544,7 @@ "xpack.securitySolution.uiSettings.enableCcsWarningDescription": "

Active les avertissements de vérification des privilèges dans les règles relatives aux index CCS

", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

Active le fil d'actualités

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "Fil d'actualités", + "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription": "[version d'évaluation technique] Activez les visualisations (analyseur et visualiseur de session) dans le menu volant.", "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel": "Activer les visualisations dans le menu volant", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzer": "Exclure les niveaux froids de l'analyseur", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzerDescription": "

Lorsque cette option est activée, les niveaux \"cold\" et \"frozen\" sont ignorés dans les requêtes de l'analyseur

", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 9b31292d397e4..ca80aaa127e32 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -42402,6 +42402,7 @@ "xpack.securitySolution.uiSettings.enableCcsWarningDescription": "

CCSインデックスのルールで権限チェック警告を有効にします

", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

ニュースフィードを有効にします

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "ニュースフィード", + "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription": "[テクニカルプレビュー]フライアウトで視覚化(アナライザーとセッションビューアー)を有効にします。", "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel": "フライアウトでビジュアライゼーションを有効化", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzer": "アナライザーでコールドティアとフローズンティアを除外", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzerDescription": "

有効にすると、アナライザークエリーでコールドティアとフローズンティアがスキップされます

", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index b89f4978302c0..31eb102ba6780 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -41786,6 +41786,7 @@ "xpack.securitySolution.uiSettings.enableCcsWarningDescription": "

在规则中为 CCS 索引启用权限检查警告

", "xpack.securitySolution.uiSettings.enableNewsFeedDescription": "

启用新闻源

", "xpack.securitySolution.uiSettings.enableNewsFeedLabel": "新闻源", + "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription": "[技术预览] 在浮出控件中启用可视化(分析器和会话查看器)。", "xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutLabel": "在浮出控件中启用可视化", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzer": "在分析器中排除冷层和冻结层", "xpack.securitySolution.uiSettings.excludeColdAndFrozenTiersInAnalyzerDescription": "

启用后,将在分析器查询中跳过冷层和冻结层

", diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts index fd42061472044..181f5c3912b14 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -44,15 +44,6 @@ export const defineGraphRoute = (router: CspRouter) => }, async (context: CspRequestHandlerContext, request, response) => { const cspContext = await context.csp; - const isGraphEnabled = await ( - await context.core - ).uiSettings.client.get(ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING); - - cspContext.logger.debug(`isGraphEnabled: ${isGraphEnabled}`); - - if (!isGraphEnabled) { - return response.notFound(); - } const { nodesLimit, showUnknownTarget = false } = request.body; const { eventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 156c4c6dc0f49..def958c07bd2d 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -196,6 +196,11 @@ export const allowedExperimentalValues = Object.freeze({ */ analyzerDatePickersAndSourcererDisabled: false, + /** + * Enables graph visualization in alerts flyout + */ + graphVisualizationInFlyoutEnabled: false, + /** * Enables an ability to customize Elastic prebuilt rules. * diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 6bd30d9061d65..5018e6ea1411a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -11,6 +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 { useDocumentDetailsContext } from '../../shared/context'; import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; import { @@ -33,6 +34,7 @@ import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { 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'; const visualizeButtons: EuiButtonGroupOptionProps[] = [ { @@ -121,14 +123,18 @@ export const VisualizeTab = memo(() => { }, [panels.left?.path?.subTab]); // Decide whether to show the graph preview or not - const { isAuditLog: isGraphPreviewEnabled } = useGraphPreview({ + const { isAuditLog } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); + const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled( + GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE + ); + const options = [...visualizeButtons]; - if (isGraphPreviewEnabled) { + if (isAuditLog && isGraphFeatureEnabled) { options.push(graphVisualizationButton); } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index 6567f2b8412b6..bc0b0b9426c8d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -21,6 +21,7 @@ import { EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID, EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID, } from '../../../shared/components/test_ids'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; const mockUseUiSetting = jest.fn().mockReturnValue([true]); jest.mock('@kbn/kibana-react-plugin/public', () => { @@ -31,6 +32,12 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { }; }); +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(), +})); + +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + jest.mock('../../shared/hooks/use_graph_preview'); jest.mock('@kbn/cloud-security-posture-graph/src/hooks', () => ({ useFetchGraphData: jest.fn(), @@ -58,6 +65,7 @@ describe('', () => { }); it('should render component and link in header', async () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, @@ -108,6 +116,7 @@ describe('', () => { }); it('should render component and without link in header in preview panel', async () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, @@ -161,6 +170,7 @@ describe('', () => { }); it('should render component and without link in header in rule preview', async () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, @@ -213,7 +223,8 @@ describe('', () => { }); }); - it('should not render when feature is not enabled', () => { + it('should not render when flyout feature is not enabled', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseUiSetting.mockReturnValue([false]); mockUseFetchGraphData.mockReturnValue({ @@ -233,7 +244,29 @@ describe('', () => { ).not.toBeInTheDocument(); }); + it('should not render when experimental feature is not enabled', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + mockUseUiSetting.mockReturnValue([true]); + + mockUseFetchGraphData.mockReturnValue({ + isLoading: false, + isError: false, + data: undefined, + }); + + (useGraphPreview as jest.Mock).mockReturnValue({ + isAuditLog: true, + }); + + const { queryByTestId } = renderGraphPreview(); + + expect( + queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + }); + it('should not render when graph data is not available', () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 82888d56edc79..cde99e88e9039 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -11,6 +11,7 @@ import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { EuiBetaBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useFetchGraphData } from '@kbn/cloud-security-posture-graph/src/hooks'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; @@ -18,6 +19,7 @@ import { GraphPreview } from './graph_preview'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useNavigateToGraphVisualization } from '../../shared/hooks/use_navigate_to_graph_visualization'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; +import { GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE } from '../../shared/constants/experimental_features'; /** * Graph preview under Overview, Visualizations. It shows a graph representation of entities. @@ -36,6 +38,9 @@ export const GraphPreviewContainer: React.FC = () => { const [visualizationInFlyoutEnabled] = useUiSetting$( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING ); + const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled( + GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE + ); const allowFlyoutExpansion = !isPreviewMode && !isPreview; @@ -55,6 +60,9 @@ export const GraphPreviewContainer: React.FC = () => { ecsData: dataAsNestedObject, }); + const shouldShowGraphPreview = + visualizationInFlyoutEnabled && isGraphFeatureEnabled && isAuditLog; + // TODO: default start and end might not capture the original event const { isLoading, isError, data } = useFetchGraphData({ req: { @@ -71,8 +79,7 @@ export const GraphPreviewContainer: React.FC = () => { }); return ( - isAuditLog && - visualizationInFlyoutEnabled && ( + shouldShowGraphPreview && ( ({ @@ -53,6 +54,12 @@ jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(), +})); + +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; + const mockUseUiSetting = jest.fn().mockReturnValue([false]); jest.mock('@kbn/kibana-react-plugin/public', () => { const original = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -132,6 +139,7 @@ describe('', () => { (useIsInvestigateInResolverActionEnabled as jest.Mock).mockReturnValue(true); (useExpandSection as jest.Mock).mockReturnValue(true); mockUseUiSetting.mockReturnValue([false]); + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); const { getByTestId, queryByTestId } = renderVisualizationsSection(); expect(getByTestId(VISUALIZATIONS_SECTION_CONTENT_TEST_ID)).toBeVisible(); @@ -144,9 +152,30 @@ describe('', () => { it('should render the graph preview component if the feature is enabled', () => { (useExpandSection as jest.Mock).mockReturnValue(true); mockUseUiSetting.mockReturnValue([true]); + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); 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', () => { + (useExpandSection as jest.Mock).mockReturnValue(true); + mockUseUiSetting.mockReturnValue([true]); + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + + const { queryByTestId } = renderVisualizationsSection(); + + expect(queryByTestId(`${GRAPH_PREVIEW_TEST_ID}LeftSection`)).not.toBeInTheDocument(); + }); + + 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); + + const { queryByTestId } = renderVisualizationsSection(); + + expect(queryByTestId(`${GRAPH_PREVIEW_TEST_ID}LeftSection`)).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index 12ed787ae9c80..f0af9ba8bb80e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -17,7 +17,9 @@ 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'; const KEY = 'visualizations'; @@ -32,12 +34,19 @@ export const VisualizationsSection = memo(() => { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING ); + const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled( + GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE + ); + // Decide whether to show the graph preview or not - const { isAuditLog: isGraphPreviewEnabled } = useGraphPreview({ + const { isAuditLog } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); + const shouldShowGraphPreview = + visualizationInFlyoutEnabled && isGraphFeatureEnabled && isAuditLog; + return ( { - {visualizationInFlyoutEnabled && isGraphPreviewEnabled && ( + {shouldShowGraphPreview && ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/experimental_features.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/experimental_features.ts new file mode 100644 index 0000000000000..aeb8b899ef6ea --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/experimental_features.ts @@ -0,0 +1,10 @@ +/* + * 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; diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index 592c648792090..6aeb86750be7a 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -209,7 +209,7 @@ export const initUiSettings = ( 'xpack.securitySolution.uiSettings.enableVisualizationsInFlyoutDescription', { defaultMessage: - '[technical preview] Enable visualizations (analyzer, session viewer, and graph view) in flyout.', + '[technical preview] Enable visualizations (analyzer and session viewer) in flyout.', values: { em: (chunks) => `${chunks}` }, } ), diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/config.ts b/x-pack/test/api_integration/apis/cloud_security_posture/config.ts index 81a2cd0df2fd5..28ac1f643041b 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/config.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/config.ts @@ -17,7 +17,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...baseConfig.get('kbnTestServer'), serverArgs: [ ...baseConfig.get('kbnTestServer.serverArgs'), - `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=false`, // Disables /graph API `--logging.loggers=${JSON.stringify([ ...getKibanaCliLoggers(baseConfig.get('kbnTestServer.serverArgs')), { diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts index bcf8ef10f483c..4ff483bff343d 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/graph.ts @@ -35,7 +35,8 @@ export default function (providerContext: FtrProviderContext) { }; describe('POST /internal/cloud_security_posture/graph', () => { - describe('Feature flag', () => { + // TODO: fix once feature flag is enabled for the API + describe.skip('Feature flag', () => { it('should return 404 when feature flag is not toggled', async () => { await postGraph(supertest, { query: { diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index 0dac59b47c52c..4e0ecd1f26e43 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -34,7 +34,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { * 1. release a new package to EPR * 2. merge the updated version number change to kibana */ - `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=true`, // Enables /graph API `--xpack.fleet.packages.0.name=cloud_security_posture`, `--xpack.fleet.packages.0.version=${CLOUD_SECURITY_PLUGIN_VERSION}`, // `--xpack.fleet.registryUrl=https://localhost:8080`, diff --git a/x-pack/test/cloud_security_posture_functional/config.ts b/x-pack/test/cloud_security_posture_functional/config.ts index db560992e4fad..bea81dd38dc15 100644 --- a/x-pack/test/cloud_security_posture_functional/config.ts +++ b/x-pack/test/cloud_security_posture_functional/config.ts @@ -39,6 +39,9 @@ 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', + ])}`, `--xpack.fleet.packages.0.name=cloud_security_posture`, `--xpack.fleet.packages.0.version=${CLOUD_SECURITY_PLUGIN_VERSION}`, // `--xpack.fleet.registryUrl=https://localhost:8080`, diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.ts index a75c3e4186a02..d5d816dfcdf17 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -33,7 +33,5 @@ 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}`, - - `--uiSettings.overrides.securitySolution:enableVisualizationsInFlyout=true`, // Enables /graph API ], }); From 6ac1a5846aa189c29771dd3ea1a22d4ee48cbff1 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 10 Dec 2024 20:55:23 +0100 Subject: [PATCH 33/36] minor fix - just for the code to make more sense --- .../right/components/graph_preview_container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index cde99e88e9039..1b3752a52553d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -42,7 +42,7 @@ export const GraphPreviewContainer: React.FC = () => { GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE ); - const allowFlyoutExpansion = !isPreviewMode && !isPreview; + const allowFlyoutExpansion = visualizationInFlyoutEnabled && !isPreviewMode && !isPreview; const { navigateToGraphVisualization } = useNavigateToGraphVisualization({ eventId, From 5e90b2c32ded284e2449c79fcfd03b8fe2c7ac38 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Tue, 10 Dec 2024 21:16:07 +0100 Subject: [PATCH 34/36] typecheck fix --- .../cloud_security_posture/server/routes/graph/route.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts index 181f5c3912b14..f9544b656f927 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -15,9 +15,6 @@ import { GRAPH_ROUTE_PATH } from '../../../common/constants'; import { CspRequestHandlerContext, CspRouter } from '../../types'; import { getGraph as getGraphV1 } from './v1'; -const ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING = - 'securitySolution:enableVisualizationsInFlyout' as const; - export const defineGraphRoute = (router: CspRouter) => router.versioned .post({ From 6a55f50192a1af2efd796f0af99c1c8ae48e9333 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Thu, 12 Dec 2024 19:08:51 +0100 Subject: [PATCH 35/36] refactoring --- .../left/components/graph_visualization.tsx | 2 + .../left/tabs/visualize_tab.tsx | 11 +- .../right/components/graph_preview.test.tsx | 24 +++- .../right/components/graph_preview.tsx | 2 +- .../graph_preview_container.test.tsx | 114 +++++++++++++----- .../components/graph_preview_container.tsx | 113 ++++++++--------- .../shared/constants/panel_keys.ts | 2 - ...e_navigate_to_graph_visualization.test.tsx | 11 +- .../use_navigate_to_graph_visualization.tsx | 9 +- 9 files changed, 168 insertions(+), 120 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index 308877fb3980c..96374b81e18d5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -20,6 +20,8 @@ const GraphInvestigationLazy = React.lazy(() => })) ); +export const GRAPH_ID = 'graph-visualization' as const; + /** * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 5018e6ea1411a..3e092c2cf05bf 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -14,10 +14,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useDocumentDetailsContext } from '../../shared/context'; import { useWhichFlyout } from '../../shared/hooks/use_which_flyout'; -import { - DocumentDetailsAnalyzerPanelKey, - VisualizationTabGraphKey, -} from '../../shared/constants/panel_keys'; +import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys'; import { VISUALIZE_TAB_BUTTON_GROUP_TEST_ID, VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID, @@ -32,7 +29,7 @@ import { import { SESSION_VIEW_ID, SessionView } from '../components/session_view'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; -import { GraphVisualization } from '../components/graph_visualization'; +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'; @@ -60,7 +57,7 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [ ]; const graphVisualizationButton: EuiButtonGroupOptionProps = { - id: VisualizationTabGraphKey, + id: GRAPH_ID, iconType: 'beaker', iconSide: 'right', toolTipProps: { @@ -159,7 +156,7 @@ export const VisualizeTab = memo(() => { {activeVisualizationId === SESSION_VIEW_ID && } {activeVisualizationId === ANALYZE_GRAPH_ID && } - {activeVisualizationId === VisualizationTabGraphKey && } + {activeVisualizationId === GRAPH_ID && } ); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.test.tsx index 22ac27eaa4e00..2142d19c82870 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.test.tsx @@ -36,10 +36,19 @@ describe('', () => { }); it('shows graph preview correctly when data is loaded', async () => { - const graphProps = { + const graphProps: GraphPreviewProps = { isLoading: false, isError: false, - data: { nodes: [], edges: [] }, + data: { + nodes: [ + { + id: '1', + color: 'primary', + shape: 'ellipse', + }, + ], + edges: [], + }, }; const { findByTestId } = renderGraphPreview(mockContextValue, graphProps); @@ -69,4 +78,15 @@ describe('', () => { expect(getByText(ERROR_MESSAGE)).toBeInTheDocument(); }); + + it('shows error message when data is empty', () => { + const graphProps = { + isLoading: false, + isError: false, + }; + + const { getByText } = renderGraphPreview(mockContextValue, graphProps); + + expect(getByText(ERROR_MESSAGE)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx index efe0a861781b8..dec3c40790ad8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview.tsx @@ -71,7 +71,7 @@ export const GraphPreview: React.FC = memo( return isLoading ? ( - ) : isError ? ( + ) : isError || memoizedNodes.length === 0 ? ( ); +const DEFAULT_NODES = [ + { + id: '1', + color: 'primary', + shape: 'ellipse', + }, +]; + describe('', () => { beforeEach(() => { jest.clearAllMocks(); + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); }); it('should render component and link in header', async () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, - data: { nodes: [], edges: [] }, + data: { nodes: DEFAULT_NODES, edges: [] }, }); const timestamp = new Date().toISOString(); @@ -116,11 +124,10 @@ describe('', () => { }); it('should render component and without link in header in preview panel', async () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, - data: { nodes: [], edges: [] }, + data: { nodes: DEFAULT_NODES, edges: [] }, }); const timestamp = new Date().toISOString(); @@ -170,11 +177,10 @@ describe('', () => { }); it('should render component and without link in header in rule preview', async () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, - data: { nodes: [], edges: [] }, + data: { nodes: DEFAULT_NODES, edges: [] }, }); const timestamp = new Date().toISOString(); @@ -223,64 +229,106 @@ describe('', () => { }); }); - it('should not render when flyout feature is not enabled', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + it('should render component and without link in header when expanding flyout feature is disabled', async () => { mockUseUiSetting.mockReturnValue([false]); - mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, - data: undefined, + data: { nodes: DEFAULT_NODES, edges: [] }, }); + const timestamp = new Date().toISOString(); + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: [], isAuditLog: true, }); - const { queryByTestId } = renderGraphPreview(); + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect(await findByTestId(GRAPH_PREVIEW_TEST_ID)).toBeInTheDocument(); expect( queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) ).not.toBeInTheDocument(); - }); - - it('should not render when experimental feature is not enabled', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - mockUseUiSetting.mockReturnValue([true]); - - mockUseFetchGraphData.mockReturnValue({ - isLoading: false, - isError: false, - data: undefined, - }); - - (useGraphPreview as jest.Mock).mockReturnValue({ - isAuditLog: true, - }); - - const { queryByTestId } = renderGraphPreview(); - expect( - queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + eventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: true, + refetchOnWindowFocus: false, + }, + }); }); - it('should not render when graph data is not available', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + it('should not render when graph data is not available', async () => { mockUseFetchGraphData.mockReturnValue({ isLoading: false, isError: false, data: undefined, }); + const timestamp = new Date().toISOString(); + (useGraphPreview as jest.Mock).mockReturnValue({ + timestamp, + eventIds: [], isAuditLog: false, }); - const { queryByTestId } = renderGraphPreview(); + const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); + // Using findByTestId to wait for the component to be rendered because it is a lazy loaded component + expect( + await findByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); expect( queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(GRAPH_PREVIEW_TEST_ID)) ).not.toBeInTheDocument(); + expect( + queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).not.toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect( + getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(GRAPH_PREVIEW_TEST_ID)) + ).toBeInTheDocument(); + expect(mockUseFetchGraphData).toHaveBeenCalled(); + expect(mockUseFetchGraphData.mock.calls[0][0]).toEqual({ + req: { + query: { + eventIds: [], + start: `${timestamp}||-30m`, + end: `${timestamp}||+30m`, + }, + }, + options: { + enabled: false, + refetchOnWindowFocus: false, + }, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 1b3752a52553d..7b8a6175d5901 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -11,7 +11,6 @@ import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { EuiBetaBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useFetchGraphData } from '@kbn/cloud-security-posture-graph/src/hooks'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_PREVIEW_TEST_ID } from './test_ids'; @@ -19,7 +18,6 @@ import { GraphPreview } from './graph_preview'; import { useGraphPreview } from '../../shared/hooks/use_graph_preview'; import { useNavigateToGraphVisualization } from '../../shared/hooks/use_navigate_to_graph_visualization'; import { ExpandablePanel } from '../../../shared/components/expandable_panel'; -import { GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE } from '../../shared/constants/experimental_features'; /** * Graph preview under Overview, Visualizations. It shows a graph representation of entities. @@ -38,10 +36,6 @@ export const GraphPreviewContainer: React.FC = () => { const [visualizationInFlyoutEnabled] = useUiSetting$( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING ); - const isGraphFeatureEnabled = useIsExperimentalFeatureEnabled( - GRAPH_VISUALIZATION_IN_FLYOUT_ENABLED_EXPERIMENTAL_FEATURE - ); - const allowFlyoutExpansion = visualizationInFlyoutEnabled && !isPreviewMode && !isPreview; const { navigateToGraphVisualization } = useNavigateToGraphVisualization({ @@ -60,9 +54,6 @@ export const GraphPreviewContainer: React.FC = () => { ecsData: dataAsNestedObject, }); - const shouldShowGraphPreview = - visualizationInFlyoutEnabled && isGraphFeatureEnabled && isAuditLog; - // TODO: default start and end might not capture the original event const { isLoading, isError, data } = useFetchGraphData({ req: { @@ -79,60 +70,58 @@ export const GraphPreviewContainer: React.FC = () => { }); return ( - shouldShowGraphPreview && ( - - ), - headerContent: ( - - ), - iconType: allowFlyoutExpansion ? 'arrowStart' : 'indexMapping', - ...(allowFlyoutExpansion && { - link: { - callback: navigateToGraphVisualization, - tooltip: ( - - ), - }, - }), - }} - data-test-subj={GRAPH_PREVIEW_TEST_ID} - content={ - !isLoading && !isError - ? { - paddingSize: 'none', + + ), + headerContent: ( + - - - ) + )} + /> + ), + iconType: allowFlyoutExpansion ? 'arrowStart' : 'indexMapping', + ...(allowFlyoutExpansion && { + link: { + callback: navigateToGraphVisualization, + tooltip: ( + + ), + }, + }), + }} + data-test-subj={GRAPH_PREVIEW_TEST_ID} + content={ + !isLoading && !isError + ? { + paddingSize: 'none', + } + : undefined + } + > + + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts index 79e603cc99c88..fa40f1e0e6674 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts @@ -12,5 +12,3 @@ export const DocumentDetailsPreviewPanelKey = 'document-details-preview' as cons export const DocumentDetailsIsolateHostPanelKey = 'document-details-isolate-host' as const; export const DocumentDetailsAlertReasonPanelKey = 'document-details-alert-reason' as const; export const DocumentDetailsAnalyzerPanelKey = 'document-details-analyzer-details' as const; - -export const VisualizationTabGraphKey = 'graph-visualization' as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx index f44a9a8e2dfa9..929dc208f3b38 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.test.tsx @@ -11,12 +11,9 @@ import { mockFlyoutApi } from '../mocks/mock_flyout_context'; import { useWhichFlyout } from './use_which_flyout'; import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; import { useKibana } from '../../../../common/lib/kibana'; -import { - DocumentDetailsRightPanelKey, - DocumentDetailsLeftPanelKey, - VisualizationTabGraphKey, -} from '../constants/panel_keys'; +import { DocumentDetailsRightPanelKey, DocumentDetailsLeftPanelKey } from '../constants/panel_keys'; import { useNavigateToGraphVisualization } from './use_navigate_to_graph_visualization'; +import { GRAPH_ID } from '../../left/components/graph_visualization'; jest.mock('@kbn/expandable-flyout'); jest.mock('../../../../common/lib/kibana'); @@ -51,7 +48,7 @@ describe('useNavigateToGraphVisualization', () => { id: DocumentDetailsLeftPanelKey, path: { tab: 'visualize', - subTab: VisualizationTabGraphKey, + subTab: GRAPH_ID, }, params: { id: eventId, @@ -84,7 +81,7 @@ describe('useNavigateToGraphVisualization', () => { id: DocumentDetailsLeftPanelKey, path: { tab: 'visualize', - subTab: VisualizationTabGraphKey, + subTab: GRAPH_ID, }, params: { id: eventId, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx index 51e74a54250ed..bb61ae6f97073 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_navigate_to_graph_visualization.tsx @@ -10,12 +10,9 @@ import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import type { Maybe } from '@kbn/timelines-plugin/common/search_strategy/common'; import { useKibana } from '../../../../common/lib/kibana'; -import { - DocumentDetailsLeftPanelKey, - DocumentDetailsRightPanelKey, - VisualizationTabGraphKey, -} from '../constants/panel_keys'; +import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey } from '../constants/panel_keys'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; +import { GRAPH_ID } from '../../left/components/graph_visualization'; export interface UseNavigateToGraphVisualizationParams { /** @@ -78,7 +75,7 @@ export const useNavigateToGraphVisualization = ({ }, path: { tab: 'visualize', - subTab: VisualizationTabGraphKey, + subTab: GRAPH_ID, }, }), [eventId, indexName, scopeId] From 40e66c4ce082e2342d101f4ce214fa98c9ecb880 Mon Sep 17 00:00:00 2001 From: Kfir Peled Date: Thu, 12 Dec 2024 21:47:35 +0100 Subject: [PATCH 36/36] refactoring --- .../graph/src/common/constants.ts | 4 + .../components/graph/use_graph_popover.tsx | 13 +- .../graph_investigation.tsx | 167 +++++++++--------- .../graph_node_expand_popover.tsx | 10 +- .../use_graph_node_expand_popover.tsx | 62 ++++--- .../graph/src/components/test_ids.ts | 2 +- .../left/tabs/visualize_tab.tsx | 4 +- .../graph_preview_container.test.tsx | 10 +- .../components/graph_preview_container.tsx | 4 +- .../visualizations_section.test.tsx | 2 +- .../components/visualizations_section.tsx | 4 +- .../shared/hooks/use_graph_preview.test.tsx | 28 +-- .../shared/hooks/use_graph_preview.ts | 8 +- 13 files changed, 158 insertions(+), 160 deletions(-) diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts index 4e02c83bf3a54..307cbd65123e4 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts @@ -6,3 +6,7 @@ */ export const EVENT_GRAPH_VISUALIZATION_API = '/internal/cloud_security_posture/graph' as const; + +export const RELATED_ENTITY = 'related.entity' as const; +export const ACTOR_ENTITY_ID = 'actor.entity.id' as const; +export const TARGET_ENTITY_ID = 'target.entity.id' as const; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx index f5bca30d1e5ae..dd8a5f0c56a72 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/use_graph_popover.tsx @@ -46,12 +46,9 @@ export const useGraphPopover = (id: string): GraphPopoverState => { const state: PopoverState = useMemo(() => ({ isOpen, anchorElement }), [isOpen, anchorElement]); - return useMemo( - () => ({ - id, - actions, - state, - }), - [id, actions, state] - ); + return { + id, + actions, + state, + }; }; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx index af5715f1c97da..081b4ec28c6a5 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -14,51 +14,21 @@ import { buildEsQuery, isCombinedFilter, buildCombinedFilter, - isPhraseFilter, + isFilter, + FilterStateStore, } from '@kbn/es-query'; -import type { Filter, Query, TimeRange, BoolQuery, PhraseFilter } from '@kbn/es-query'; +import type { Filter, Query, TimeRange, PhraseFilter } from '@kbn/es-query'; import { css } from '@emotion/react'; import { getEsQueryConfig } from '@kbn/data-service'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Graph, type NodeViewModel } from '../../..'; +import { Graph } from '../../..'; import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; import { useFetchGraphData } from '../../hooks/use_fetch_graph_data'; import { GRAPH_INVESTIGATION_TEST_ID } from '../test_ids'; +import { ACTOR_ENTITY_ID, RELATED_ENTITY, TARGET_ENTITY_ID } from '../../common/constants'; const CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER = 'graph-investigation'; -const useTimeRange = (timestamp: string) => { - const [timeRange, setTimeRange] = useState({ - from: `${timestamp}||-30m`, - to: `${timestamp}||+30m`, - }); - - const setPartialTimeRange = (newTimeRange: Partial) => { - setTimeRange((currTimeRange) => ({ ...currTimeRange, ...newTimeRange })); - }; - - return { timeRange, setTimeRange, setPartialTimeRange }; -}; - -const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: BoolQuery }) => { - const { data, refresh, isFetching } = useFetchGraphData({ - req: { - query: { - eventIds, - esQuery: filter, - start: timeRange.from, - end: timeRange.to, - }, - }, - options: { - refetchOnWindowFocus: false, - keepPreviousData: true, - }, - }); - - return { data, refresh, isFetching }; -}; - const buildPhraseFilter = (field: string, value: string, dataViewId?: string): PhraseFilter => ({ meta: { key: field, @@ -79,6 +49,16 @@ const buildPhraseFilter = (field: string, value: string, dataViewId?: string): P }, }); +/** + * Adds a filter to the existing list of filters based on the provided key and value. + * It will always use the first filter in the list to build a combined filter with the new filter. + * + * @param dataViewId - The ID of the data view to which the filter belongs. + * @param prev - The previous list of filters. + * @param key - The key for the filter. + * @param value - The value for the filter. + * @returns A new list of filters with the added filter. + */ const addFilter = (dataViewId: string, prev: Filter[], key: string, value: string) => { const [firstFilter, ...otherFilters] = prev; @@ -96,7 +76,7 @@ const addFilter = (dataViewId: string, prev: Filter[], key: string, value: strin }, ...otherFilters, ]; - } else if (isPhraseFilter(firstFilter)) { + } else if (isFilter(firstFilter) && firstFilter.meta?.type !== 'custom') { return [ buildCombinedFilter(BooleanRelation.OR, [firstFilter, buildPhraseFilter(key, value)], { id: dataViewId, @@ -104,7 +84,15 @@ const addFilter = (dataViewId: string, prev: Filter[], key: string, value: strin ...otherFilters, ]; } else { - return [buildPhraseFilter(key, value, dataViewId), ...prev]; + return [ + { + $state: { + store: FilterStateStore.APP_STATE, + }, + ...buildPhraseFilter(key, value, dataViewId), + }, + ...prev, + ]; } }; @@ -114,43 +102,27 @@ const useGraphPopovers = ( ) => { const nodeExpandPopover = useGraphNodeExpandPopover({ onExploreRelatedEntitiesClick: (node) => { - setSearchFilters((prev) => addFilter(dataViewId, prev, 'related.entity', node.id)); + setSearchFilters((prev) => addFilter(dataViewId, prev, RELATED_ENTITY, node.id)); }, onShowActionsByEntityClick: (node) => { - setSearchFilters((prev) => addFilter(dataViewId, prev, 'actor.entity.id', node.id)); + setSearchFilters((prev) => addFilter(dataViewId, prev, ACTOR_ENTITY_ID, node.id)); }, onShowActionsOnEntityClick: (node) => { - setSearchFilters((prev) => addFilter(dataViewId, prev, 'target.entity.id', node.id)); + setSearchFilters((prev) => addFilter(dataViewId, prev, TARGET_ENTITY_ID, node.id)); }, }); - const popovers = [nodeExpandPopover]; - const popoverOpenWrapper = (cb: Function, ...args: unknown[]) => { - popovers.forEach(({ actions: { closePopover } }) => { - closePopover(); - }); - cb(...args); - }; + const openPopoverCallback = useCallback( + (cb: Function, ...args: unknown[]) => { + [nodeExpandPopover].forEach(({ actions: { closePopover } }) => { + closePopover(); + }); + cb(...args); + }, + [nodeExpandPopover] + ); - return { nodeExpandPopover, popoverOpenWrapper }; -}; - -const useGraphNodes = ( - nodes: NodeViewModel[], - expandButtonClickHandler: (...args: unknown[]) => void -) => { - return useMemo(() => { - return nodes.map((node) => { - const nodeHandlers = - node.shape !== 'label' && node.shape !== 'group' - ? { - expandButtonClick: expandButtonClickHandler, - } - : undefined; - return { ...node, ...nodeHandlers }; - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nodes]); + return { nodeExpandPopover, openPopoverCallback }; }; interface GraphInvestigationProps { @@ -163,42 +135,63 @@ interface GraphInvestigationProps { * Graph investigation view allows the user to expand nodes and view related entities. */ export const GraphInvestigation: React.FC = memo( - ({ dataView, eventIds, timestamp }: GraphInvestigationProps) => { + ({ dataView, eventIds, timestamp = new Date().toISOString() }: GraphInvestigationProps) => { const [searchFilters, setSearchFilters] = useState(() => []); - const { timeRange, setTimeRange } = useTimeRange(timestamp ?? new Date().toISOString()); + const [timeRange, setTimeRange] = useState({ + from: `${timestamp}||-30m`, + to: `${timestamp}||+30m`, + }); const { services: { uiSettings }, } = useKibana(); - const [query, setQuery] = useState<{ bool: BoolQuery }>( - buildEsQuery( - dataView, - [], - [...searchFilters], - getEsQueryConfig(uiSettings as Parameters[0]) - ) - ); - - useEffect(() => { - setQuery( + const query = useMemo( + () => buildEsQuery( dataView, [], [...searchFilters], getEsQueryConfig(uiSettings as Parameters[0]) - ) - ); - }, [searchFilters, dataView, uiSettings]); + ), + [searchFilters, dataView, uiSettings] + ); - const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers( + const { nodeExpandPopover, openPopoverCallback } = useGraphPopovers( dataView?.id ?? '', setSearchFilters ); const expandButtonClickHandler = (...args: unknown[]) => - popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); + openPopoverCallback(nodeExpandPopover.onNodeExpandButtonClick, ...args); const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); - const { data, refresh, isFetching } = useGraphData(eventIds, timeRange, query); - const nodes = useGraphNodes(data?.nodes ?? [], expandButtonClickHandler); + const { data, refresh, isFetching } = useFetchGraphData({ + req: { + query: { + eventIds, + esQuery: query, + start: timeRange.from, + end: timeRange.to, + }, + }, + options: { + refetchOnWindowFocus: false, + keepPreviousData: true, + }, + }); + + const nodes = useMemo(() => { + return ( + data?.nodes.map((node) => { + const nodeHandlers = + node.shape !== 'label' && node.shape !== 'group' + ? { + expandButtonClick: expandButtonClickHandler, + } + : undefined; + return { ...node, ...nodeHandlers }; + }) ?? [] + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data?.nodes]); return ( <> diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx index 45cd5a167ffa5..c22f8dbe51ace 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx @@ -12,7 +12,7 @@ import { ExpandPopoverListItem } from '../styles'; import { GraphPopover } from '../../..'; import { GRAPH_NODE_EXPAND_POPOVER_TEST_ID, - GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID, + GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID, GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID, GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID, } from '../test_ids'; @@ -21,7 +21,7 @@ interface GraphNodeExpandPopoverProps { isOpen: boolean; anchorElement: HTMLElement | null; closePopover: () => void; - onExploreRelatedEntitiesClick: () => void; + onShowRelatedEntitiesClick: () => void; onShowActionsByEntityClick: () => void; onShowActionsOnEntityClick: () => void; } @@ -31,7 +31,7 @@ export const GraphNodeExpandPopover: React.FC = mem isOpen, anchorElement, closePopover, - onExploreRelatedEntitiesClick, + onShowRelatedEntitiesClick, onShowActionsByEntityClick, onShowActionsOnEntityClick, }) => { @@ -66,8 +66,8 @@ export const GraphNodeExpandPopover: React.FC = mem label={i18n.translate('xpack.csp.graph.graphNodeExpandPopover.showRelatedEvents', { defaultMessage: 'Show related events', })} - onClick={onExploreRelatedEntitiesClick} - data-test-subj={GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID} + onClick={onShowRelatedEntitiesClick} + data-test-subj={GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID} />
diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx index 7178f5f9dbca7..90e8f66510cc0 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph_investigation/use_graph_node_expand_popover.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { memo, useCallback, useRef, useState } from 'react'; import { useGraphPopover } from '../../..'; import type { ExpandButtonClickCallback, NodeProps } from '../types'; import { GraphNodeExpandPopover } from './graph_node_expand_popover'; @@ -32,6 +32,7 @@ export const useGraphNodeExpandPopover = ({ unToggleCallback: () => void; } | null>(null); + // Handler to close the popover, reset selected node and unToggle callback const closePopoverHandler = useCallback(() => { selectedNode.current = null; unToggleCallbackRef.current?.(); @@ -39,6 +40,11 @@ export const useGraphNodeExpandPopover = ({ closePopover(); }, [closePopover]); + /** + * Handles the click event on the node expand button. + * Closes the current popover if open and sets the pending open state + * if the clicked node is different from the currently selected node. + */ const onNodeExpandButtonClick: ExpandButtonClickCallback = useCallback( (e, node, unToggleCallback) => { // Close the current popover if open @@ -52,25 +58,14 @@ export const useGraphNodeExpandPopover = ({ [closePopoverHandler] ); - useEffect(() => { - // Open pending popover if the popover is not open - if (!state.isOpen && pendingOpen) { - const { node, el, unToggleCallback } = pendingOpen; - - selectedNode.current = node; - unToggleCallbackRef.current = unToggleCallback; - openPopover(el); - - setPendingOpen(null); - } - }, [state.isOpen, pendingOpen, openPopover]); - + // PopoverComponent is a memoized component that renders the GraphNodeExpandPopover + // It handles the display of the popover and the actions that can be performed on the node const PopoverComponent = memo(() => ( { + onShowRelatedEntitiesClick={() => { onExploreRelatedEntitiesClick(selectedNode.current as NodeProps); closePopoverHandler(); }} @@ -85,19 +80,28 @@ export const useGraphNodeExpandPopover = ({ /> )); - PopoverComponent.displayName = GraphNodeExpandPopover.displayName; + // Open pending popover if the popover is not open + // This block checks if there is a pending popover to be opened. + // If the popover is not currently open and there is a pending popover, + // it sets the selected node, stores the unToggle callback, and opens the popover. + if (!state.isOpen && pendingOpen) { + const { node, el, unToggleCallback } = pendingOpen; - return useMemo( - () => ({ - onNodeExpandButtonClick, - PopoverComponent, - id, - actions: { - ...actions, - closePopover: closePopoverHandler, - }, - state, - }), - [PopoverComponent, actions, closePopoverHandler, id, onNodeExpandButtonClick, state] - ); + selectedNode.current = node; + unToggleCallbackRef.current = unToggleCallback; + openPopover(el); + + setPendingOpen(null); + } + + return { + onNodeExpandButtonClick, + PopoverComponent, + id, + actions: { + ...actions, + closePopover: closePopoverHandler, + }, + state, + }; }; diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts index 2fed9486840b2..96e399d670907 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/test_ids.ts @@ -10,7 +10,7 @@ export const PREFIX = 'cloudSecurityGraph' as const; export const GRAPH_INVESTIGATION_TEST_ID = `${PREFIX}GraphInvestigation` as const; export const GRAPH_NODE_EXPAND_POPOVER_TEST_ID = `${GRAPH_INVESTIGATION_TEST_ID}GraphNodeExpandPopover` as const; -export const GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID = +export const GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID = `${GRAPH_INVESTIGATION_TEST_ID}ExploreRelatedEntities` as const; export const GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID = `${GRAPH_INVESTIGATION_TEST_ID}ShowActionsByEntity` as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx index 3e092c2cf05bf..89e00e06e3a49 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/tabs/visualize_tab.tsx @@ -120,7 +120,7 @@ export const VisualizeTab = memo(() => { }, [panels.left?.path?.subTab]); // Decide whether to show the graph preview or not - const { isAuditLog } = useGraphPreview({ + const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); @@ -131,7 +131,7 @@ export const VisualizeTab = memo(() => { const options = [...visualizeButtons]; - if (isAuditLog && isGraphFeatureEnabled) { + if (hasGraphRepresentation && isGraphFeatureEnabled) { options.push(graphVisualizationButton); } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx index ce836d9402816..c805f2a3c67a7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.test.tsx @@ -85,7 +85,7 @@ describe('', () => { (useGraphPreview as jest.Mock).mockReturnValue({ timestamp, eventIds: [], - isAuditLog: true, + hasGraphRepresentation: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -135,7 +135,7 @@ describe('', () => { (useGraphPreview as jest.Mock).mockReturnValue({ timestamp, eventIds: [], - isAuditLog: true, + hasGraphRepresentation: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ @@ -188,7 +188,7 @@ describe('', () => { (useGraphPreview as jest.Mock).mockReturnValue({ timestamp, eventIds: [], - isAuditLog: true, + hasGraphRepresentation: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview({ @@ -242,7 +242,7 @@ describe('', () => { (useGraphPreview as jest.Mock).mockReturnValue({ timestamp, eventIds: [], - isAuditLog: true, + hasGraphRepresentation: true, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); @@ -292,7 +292,7 @@ describe('', () => { (useGraphPreview as jest.Mock).mockReturnValue({ timestamp, eventIds: [], - isAuditLog: false, + hasGraphRepresentation: false, }); const { getByTestId, queryByTestId, findByTestId } = renderGraphPreview(); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index 7b8a6175d5901..90a0218778549 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -48,7 +48,7 @@ export const GraphPreviewContainer: React.FC = () => { const { eventIds, timestamp = new Date().toISOString(), - isAuditLog, + hasGraphRepresentation, } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, @@ -64,7 +64,7 @@ export const GraphPreviewContainer: React.FC = () => { }, }, options: { - enabled: isAuditLog, + enabled: hasGraphRepresentation, refetchOnWindowFocus: false, }, }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index d662f666628b7..6fb4d5d30b897 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -105,7 +105,7 @@ describe('', () => { statsNodes: undefined, }); mockUseGraphPreview.mockReturnValue({ - isAuditLog: true, + hasGraphRepresentation: true, }); mockUseFetchGraphData.mockReturnValue({ isLoading: false, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx index f0af9ba8bb80e..23bea1f8fecdd 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.tsx @@ -39,13 +39,13 @@ export const VisualizationsSection = memo(() => { ); // Decide whether to show the graph preview or not - const { isAuditLog } = useGraphPreview({ + const { hasGraphRepresentation } = useGraphPreview({ getFieldsData, ecsData: dataAsNestedObject, }); const shouldShowGraphPreview = - visualizationInFlyoutEnabled && isGraphFeatureEnabled && isAuditLog; + visualizationInFlyoutEnabled && isGraphFeatureEnabled && hasGraphRepresentation; return ( { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(false); + expect(hasGraphRepresentation).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual([]); @@ -67,9 +67,9 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(false); + expect(hasGraphRepresentation).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); @@ -94,9 +94,9 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(false); + expect(hasGraphRepresentation).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); @@ -125,9 +125,9 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(false); + expect(hasGraphRepresentation).toEqual(false); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual([]); expect(actorIds).toEqual(['actorId']); @@ -156,9 +156,9 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(false); + expect(hasGraphRepresentation).toEqual(false); expect(timestamp).toEqual(null); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); @@ -179,9 +179,9 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(true); + expect(hasGraphRepresentation).toEqual(true); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['eventId']); expect(actorIds).toEqual(['actorId']); @@ -214,9 +214,9 @@ describe('useGraphPreview', () => { }, }); - const { isAuditLog, timestamp, eventIds, actorIds, action, targetIds } = + const { hasGraphRepresentation, timestamp, eventIds, actorIds, action, targetIds } = hookResult.result.current; - expect(isAuditLog).toEqual(true); + expect(hasGraphRepresentation).toEqual(true); expect(timestamp).toEqual(mockFieldData['@timestamp'][0]); expect(eventIds).toEqual(['id1', 'id2']); expect(actorIds).toEqual(['actorId1', 'actorId2']); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts index d41e905c80703..48233afab02df 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_graph_preview.ts @@ -51,9 +51,9 @@ export interface UseGraphPreviewResult { action?: string[]; /** - * Boolean indicating if the event is an audit log (contains event ids, actor ids and action) + * Boolean indicating if the event is has a graph representation (contains event ids, actor ids and action) */ - isAuditLog: boolean; + hasGraphRepresentation: boolean; } /** @@ -71,12 +71,12 @@ export const useGraphPreview = ({ const actorIds = getFieldArray(getFieldsData('actor.entity.id')); const targetIds = getFieldArray(getFieldsData('target.entity.id')); const action: string[] | undefined = get(['event', 'action'], ecsData); - const isAuditLog = + const hasGraphRepresentation = Boolean(timestamp) && Boolean(action?.length) && actorIds.length > 0 && eventIds.length > 0 && targetIds.length > 0; - return { timestamp, eventIds, actorIds, action, targetIds, isAuditLog }; + return { timestamp, eventIds, actorIds, action, targetIds, hasGraphRepresentation }; };