From 30143dba5d38968d50c1964812181bb1d6f5e400 Mon Sep 17 00:00:00 2001 From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:54:09 +0000 Subject: [PATCH] [Cloud Security] Added popover support for graph component (#199053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Added popover support to the graph component. In order to scale the rendering component of nodes, we prefer not to add popover per node but to manage a single popover for each use-case. In the popover stories you can see an example of two different popovers being triggered by different buttons on the node.
Popover support 📹 https://github.com/user-attachments/assets/cb5bc2ce-037a-4f9b-b71a-f95a9362dde0
Dark mode support 📹 https://github.com/user-attachments/assets/a55f2a88-ed07-40e2-9404-30a2042bf4fc
### How to test To test this PR you can run ``` yarn storybook cloud_security_posture_packages ``` And to test the alerts flyout (for regression test): Toggle feature flag in kibana.dev.yml ```yaml xpack.securitySolution.enableExperimental: ['graphVisualizationInFlyoutEnabled'] ``` Load mocked data ```bash node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 ``` 1. Go to the alerts page 2. Change the query time range to show alerts from the 13th of October 2024 (**IMPORTANT**) 3. Open the alerts flyout 5. Scroll to see the graph visualization : D ### Related PRs - https://github.com/elastic/kibana/pull/196034 - https://github.com/elastic/kibana/pull/195307 ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) (cherry picked from commit f3de5930493fb34a174e14f6dabdc1faf0722cad) --- .../graph/src/components/edge/styles.tsx | 2 +- .../graph/src/components/graph/graph.tsx | 183 +++-- .../graph/graph_popover.stories.tsx | 209 ++++++ .../src/components/graph/graph_popover.tsx | 85 +++ .../components/graph/use_graph_popover.tsx | 57 ++ .../graph/src/components/index.ts | 2 + .../src/components/node/diamond_node.tsx | 16 +- .../src/components/node/ellipse_node.tsx | 16 +- .../src/components/node/hexagon_node.tsx | 16 +- .../components/node/node_expand_button.tsx | 43 ++ .../src/components/node/pentagon_node.tsx | 16 +- .../src/components/node/rectangle_node.tsx | 16 +- .../graph/src/components/node/styles.tsx | 110 +-- .../graph/src/components/styles.tsx | 80 ++ .../graph/src/components/types.ts | 14 +- .../es_archives/security_alerts/data.json | 708 ++++++++++++++++++ .../es_archives/security_alerts/data.json.gz | Bin 3143 -> 0 bytes .../pages/alerts_flyout.ts | 4 +- 18 files changed, 1422 insertions(+), 155 deletions(-) create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.stories.tsx 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/node/node_expand_button.tsx create mode 100644 x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx create mode 100644 x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json delete mode 100644 x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json.gz 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 ( - + 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..0b956cb19e10d 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, useRef } from 'react'; +import { size, isEmpty, isEqual, xorWith } from 'lodash'; import { Background, Controls, @@ -14,7 +15,8 @@ import { useEdgesState, useNodesState, } from '@xyflow/react'; -import type { Edge, Node } from '@xyflow/react'; +import type { Edge, FitViewOptions, Node, ReactFlowInstance } from '@xyflow/react'; +import { useGeneratedHtmlId } from '@elastic/eui'; import type { CommonProps } from '@elastic/eui'; import { SvgDefsMarker } from '../edge/styles'; import { @@ -33,9 +35,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 +82,47 @@ 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 = false, + ...rest +}) => { + const backgroundId = useGeneratedHtmlId(); + const fitViewRef = useRef< + ((fitViewOptions?: FitViewOptions | undefined) => Promise) | null + >(null); + const currNodesRef = useRef([]); + const currEdgesRef = useRef([]); + const [isGraphInteractive, setIsGraphInteractive] = 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, currNodesRef.current) || + !isArrayOfObjectsEqual(edges, currEdgesRef.current) + ) { + const { initialNodes, initialEdges } = processGraph(nodes, edges, isGraphInteractive); + const { nodes: layoutedNodes } = layoutGraph(initialNodes, initialEdges); + + setNodes(layoutedNodes); + setEdges(initialEdges); + currNodesRef.current = nodes; + currEdgesRef.current = edges; + setTimeout(() => { + fitViewRef.current?.(); + }, 30); + } + }, [nodes, edges, setNodes, setEdges, isGraphInteractive]); const onInteractiveStateChange = useCallback( (interactiveStatus: boolean): void => { - setIsGraphLocked(interactiveStatus); - setNodes((prevNodes) => - prevNodes.map((node) => ({ + setIsGraphInteractive(interactiveStatus); + setNodes((currNodes) => + currNodes.map((node) => ({ ...node, data: { ...node.data, @@ -99,23 +134,29 @@ export const Graph: React.FC = ({ nodes, edges, interactive, ...rest [setNodes] ); + const onInitCallback = useCallback( + (xyflow: ReactFlowInstance, Edge>) => { + 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) { + const resizeObserver = new ResizeObserver(() => { + xyflow.fitView(); + }); + resizeObserver.observe(document.querySelector('.react-flow') as Element); + return () => resizeObserver.disconnect(); + } + }, + [interactive] + ); + return (
{ - window.requestAnimationFrame(() => xyflow.fitView()); - - // When the graph is not initialized as interactive, we need to fit the view on resize - if (!interactive) { - const resizeObserver = new ResizeObserver(() => { - xyflow.fitView(); - }); - resizeObserver.observe(document.querySelector('.react-flow') as Element); - return () => resizeObserver.disconnect(); - } - }} + onInit={onInitCallback} nodeTypes={nodeTypes} edgeTypes={edgeTypes} nodes={nodesState} @@ -123,16 +164,17 @@ 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={isGraphInteractive && !isLocked} + zoomOnScroll={isGraphInteractive && !isLocked} + zoomOnPinch={isGraphInteractive && !isLocked} + zoomOnDoubleClick={isGraphInteractive && !isLocked} + preventScrolling={interactive} + nodesDraggable={interactive && isGraphInteractive && !isLocked} maxZoom={1.3} + minZoom={0.1} > {interactive && } - + {' '}
); @@ -173,32 +215,41 @@ const processGraph = ( return node; }); - const initialEdges: Array> = edgesModel.map((edgeData) => { - const isIn = - nodesById[edgeData.source].shape !== 'label' && nodesById[edgeData.target].shape === 'group'; - const isInside = - nodesById[edgeData.source].shape === 'group' && nodesById[edgeData.target].shape === 'label'; - const isOut = - nodesById[edgeData.source].shape === 'label' && nodesById[edgeData.target].shape === 'group'; - const isOutside = - nodesById[edgeData.source].shape === 'group' && nodesById[edgeData.target].shape !== 'label'; - - return { - id: edgeData.id, - type: 'default', - source: edgeData.source, - sourceHandle: isInside ? 'inside' : isOutside ? 'outside' : undefined, - target: edgeData.target, - targetHandle: isIn ? 'in' : isOut ? 'out' : undefined, - focusable: false, - selectable: false, - data: { - ...edgeData, - sourceShape: nodesById[edgeData.source].shape, - targetShape: nodesById[edgeData.target].shape, - }, - }; - }); + const initialEdges: Array> = edgesModel + .filter((edgeData) => nodesById[edgeData.source] && nodesById[edgeData.target]) + .map((edgeData) => { + const isIn = + nodesById[edgeData.source].shape !== 'label' && + nodesById[edgeData.target].shape === 'group'; + const isInside = + nodesById[edgeData.source].shape === 'group' && + nodesById[edgeData.target].shape === 'label'; + const isOut = + nodesById[edgeData.source].shape === 'label' && + nodesById[edgeData.target].shape === 'group'; + const isOutside = + nodesById[edgeData.source].shape === 'group' && + nodesById[edgeData.target].shape !== 'label'; + + return { + id: edgeData.id, + type: 'default', + source: edgeData.source, + sourceHandle: isInside ? 'inside' : isOutside ? 'outside' : undefined, + target: edgeData.target, + targetHandle: isIn ? 'in' : isOut ? 'out' : undefined, + focusable: false, + selectable: false, + data: { + ...edgeData, + sourceShape: nodesById[edgeData.source].shape, + targetShape: nodesById[edgeData.target].shape, + }, + }; + }); 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.stories.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.stories.tsx new file mode 100644 index 0000000000000..6d5b3c1b372fc --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.stories.tsx @@ -0,0 +1,209 @@ +/* + * 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 } from '@elastic/eui'; +import type { EntityNodeViewModel, NodeProps } from '..'; +import { Graph } from '..'; +import { GraphPopover } from './graph_popover'; +import { ExpandButtonClickCallback } from '../types'; +import { useGraphPopover } from './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 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/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..570c1332a8834 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -0,0 +1,85 @@ +/* + * 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, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export interface GraphPopoverProps + extends PropsWithChildren, + CommonProps, + Pick< + EuiWrappingPopoverProps, + 'anchorPosition' | 'panelClassName' | 'panelPaddingSize' | 'panelStyle' + > { + isOpen: boolean; + anchorElement: HTMLElement | null; + closePopover: () => void; +} + +export const GraphPopover: React.FC = ({ + isOpen, + anchorElement, + closePopover, + children, + ...rest +}) => { + const { euiTheme } = useEuiTheme(); + + if (!anchorElement) { + return null; + } + + return ( + { + anchorElement.focus(); + return false; + }, + preventScrollOnFocus: true, + onClickOutside: () => { + 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/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/node/diamond_node.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx index f96068061a433..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 @@ -19,12 +19,13 @@ import { HandleStyleOverride, } 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 ( @@ -55,11 +56,14 @@ export const EllipseNode: 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 HexagonNode: React.FC = 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((currIsToggled) => !currIsToggled); + 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..f2745cef7ec80 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 @@ -20,6 +20,7 @@ import { } 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/styles.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx new file mode 100644 index 0000000000000..0efff1c88456c --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/styles.tsx @@ -0,0 +1,80 @@ +/* + * 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'; + +interface EuiColorProps { + color: keyof ReturnType['euiTheme']['colors']; + background: _EuiBackgroundColor; +} + +type IconContainerProps = EuiColorProps; + +const IconContainer = styled.div` + position: relative; + width: 24px; + height: 24px; + border-radius: 50%; + color: ${({ color }) => { + const { euiTheme } = useEuiTheme(); + return euiTheme.colors[color]; + }}; + background-color: ${({ background }) => useEuiBackgroundColor(background)}; + border: 1px solid + ${({ color }) => { + const { euiTheme } = useEuiTheme(); + return euiTheme.colors[color]; + }}; + margin-right: 8px; +`; + +const StyleEuiIcon = styled(EuiIcon)` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +type RoundedEuiIconProps = EuiIconProps & EuiColorProps; + +const RoundedEuiIcon: React.FC = ({ color, background, ...rest }) => ( + + + +); + +export const ExpandPopoverListItem: React.FC< + Pick +> = (props) => { + const { euiTheme } = useEuiTheme(); + return ( + + ) : undefined + } + label={ + + {props.label} + + } + onClick={props.onClick} + /> + ); +}; 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..328829ee3fabe 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 @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import type { EntityNodeDataModel, GroupNodeDataModel, @@ -24,11 +25,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 @@ -40,7 +50,7 @@ export interface LabelNodeViewModel extends Record, LabelNodeDataModel, BaseNodeDataViewModel { - expandButtonClick?: (e: React.MouseEvent, node: NodeProps) => void; + expandButtonClick?: ExpandButtonClickCallback; } export type NodeViewModel = EntityNodeViewModel | GroupNodeViewModel | LabelNodeViewModel; diff --git a/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json b/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json new file mode 100644 index 0000000000000..94ecc85bfd234 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json @@ -0,0 +1,708 @@ +{ + "type": "doc", + "value": { + "id": "589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1", + "index": ".internal.alerts-security.alerts-default-000001", + "source": { + "@timestamp": "2024-09-01T20:44:02.109Z", + "actor": { + "entity": { + "id": "admin@example.com" + } + }, + "client": { + "user": { + "email": "admin@example.com" + } + }, + "cloud": { + "project": { + "id": "your-project-id" + }, + "provider": "gcp" + }, + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "google.iam.admin.v1.CreateRole", + "agent_id_status": "missing", + "category": [ + "session", + "network", + "configuration" + ], + "dataset": "gcp.audit", + "id": "kabcd1234efgh5678", + "ingested": "2024-09-01T20:40:17Z", + "module": "gcp", + "outcome": "success", + "provider": "activity", + "type": [ + "end", + "access", + "allowed" + ] + }, + "event.kind": "signal", + "gcp": { + "audit": { + "authorization_info": [ + { + "granted": true, + "permission": "iam.roles.create", + "resource": "projects/your-project-id" + } + ], + "logentry_operation": { + "id": "operation-0987654321" + }, + "request": { + "@type": "type.googleapis.com/google.iam.admin.v1.CreateRoleRequest", + "parent": "projects/your-project-id", + "role": { + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "title": "Custom Role" + }, + "roleId": "customRole" + }, + "resource_name": "projects/your-project-id/roles/customRole", + "response": { + "@type": "type.googleapis.com/google.iam.admin.v1.Role", + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "stage": "GA", + "title": "Custom Role" + }, + "type": "type.googleapis.com/google.cloud.audit.AuditLog" + } + }, + "kibana.alert.ancestors": [ + { + "depth": 0, + "id": "MhKch5IBGYRrfvcTQNbO", + "index": ".ds-logs-gcp.audit-default-2024.10.13-000001", + "type": "event" + } + ], + "kibana.alert.depth": 1, + "kibana.alert.intended_timestamp": "2024-09-01T20:44:02.117Z", + "kibana.alert.last_detected": "2024-09-01T20:44:02.117Z", + "kibana.alert.original_event.action": "google.iam.admin.v1.CreateRole", + "kibana.alert.original_event.agent_id_status": "missing", + "kibana.alert.original_event.category": [ + "session", + "network", + "configuration" + ], + "kibana.alert.original_event.dataset": "gcp.audit", + "kibana.alert.original_event.id": "kabcd1234efgh5678", + "kibana.alert.original_event.ingested": "2024-09-01T20:40:17Z", + "kibana.alert.original_event.kind": "event", + "kibana.alert.original_event.module": "gcp", + "kibana.alert.original_event.outcome": "success", + "kibana.alert.original_event.provider": "activity", + "kibana.alert.original_event.type": [ + "end", + "access", + "allowed" + ], + "kibana.alert.original_time": "2024-09-01T12:34:56.789Z", + "kibana.alert.reason": "session, network, configuration event with source 10.0.0.1 created medium alert GCP IAM Custom Role Creation.", + "kibana.alert.risk_score": 47, + "kibana.alert.rule.actions": [ + ], + "kibana.alert.rule.author": [ + "Elastic" + ], + "kibana.alert.rule.category": "Custom Query Rule", + "kibana.alert.rule.consumer": "siem", + "kibana.alert.rule.created_at": "2024-09-01T20:38:49.650Z", + "kibana.alert.rule.created_by": "elastic", + "kibana.alert.rule.description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.", + "kibana.alert.rule.enabled": true, + "kibana.alert.rule.exceptions_list": [ + ], + "kibana.alert.rule.execution.timestamp": "2024-09-01T20:44:02.117Z", + "kibana.alert.rule.execution.uuid": "a440f349-1900-4087-b507-f2b98c6cfa79", + "kibana.alert.rule.false_positives": [ + "Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "kibana.alert.rule.from": "now-6m", + "kibana.alert.rule.immutable": true, + "kibana.alert.rule.indices": [ + "filebeat-*", + "logs-gcp*" + ], + "kibana.alert.rule.interval": "5m", + "kibana.alert.rule.license": "Elastic License v2", + "kibana.alert.rule.max_signals": 100, + "kibana.alert.rule.name": "GCP IAM Custom Role Creation", + "kibana.alert.rule.note": "", + "kibana.alert.rule.parameters": { + "author": [ + "Elastic" + ], + "description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.", + "exceptions_list": [ + ], + "false_positives": [ + "Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-6m", + "immutable": true, + "index": [ + "filebeat-*", + "logs-gcp*" + ], + "language": "kuery", + "license": "Elastic License v2", + "max_signals": 100, + "note": "", + "query": "event.dataset:gcp.audit and event.action:google.iam.admin.v*.CreateRole and event.outcome:success\n", + "references": [ + "https://cloud.google.com/iam/docs/understanding-custom-roles" + ], + "related_integrations": [ + { + "integration": "audit", + "package": "gcp", + "version": "^2.0.0" + } + ], + "required_fields": [ + { + "ecs": true, + "name": "event.action", + "type": "keyword" + }, + { + "ecs": true, + "name": "event.dataset", + "type": "keyword" + }, + { + "ecs": true, + "name": "event.outcome", + "type": "keyword" + } + ], + "risk_score": 47, + "risk_score_mapping": [ + ], + "rule_id": "aa8007f0-d1df-49ef-8520-407857594827", + "rule_source": { + "is_customized": false, + "type": "external" + }, + "setup": "The GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "severity": "medium", + "severity_mapping": [ + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "to": "now", + "type": "query", + "version": 104 + }, + "kibana.alert.rule.producer": "siem", + "kibana.alert.rule.references": [ + "https://cloud.google.com/iam/docs/understanding-custom-roles" + ], + "kibana.alert.rule.revision": 0, + "kibana.alert.rule.risk_score": 47, + "kibana.alert.rule.risk_score_mapping": [ + ], + "kibana.alert.rule.rule_id": "aa8007f0-d1df-49ef-8520-407857594827", + "kibana.alert.rule.rule_type_id": "siem.queryRule", + "kibana.alert.rule.severity": "medium", + "kibana.alert.rule.severity_mapping": [ + ], + "kibana.alert.rule.tags": [ + "Domain: Cloud", + "Data Source: GCP", + "Data Source: Google Cloud Platform", + "Use Case: Identity and Access Audit", + "Tactic: Initial Access" + ], + "kibana.alert.rule.threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "kibana.alert.rule.timestamp_override": "event.ingested", + "kibana.alert.rule.to": "now", + "kibana.alert.rule.type": "query", + "kibana.alert.rule.updated_at": "2024-09-01T20:39:00.099Z", + "kibana.alert.rule.updated_by": "elastic", + "kibana.alert.rule.uuid": "c6f64115-5941-46ef-bfa3-61a4ecb4f3ba", + "kibana.alert.rule.version": 104, + "kibana.alert.severity": "medium", + "kibana.alert.start": "2024-09-01T20:44:02.117Z", + "kibana.alert.status": "active", + "kibana.alert.uuid": "589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1", + "kibana.alert.workflow_assignee_ids": [ + ], + "kibana.alert.workflow_status": "open", + "kibana.alert.workflow_tags": [ + ], + "kibana.space_ids": [ + "default" + ], + "kibana.version": "9.0.0", + "log": { + "level": "NOTICE", + "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" + }, + "related": { + "ip": [ + "10.0.0.1" + ], + "user": [ + "admin@example.com" + ] + }, + "service": { + "name": "iam.googleapis.com" + }, + "source": { + "ip": "10.0.0.1" + }, + "tags": [ + "_geoip_database_unavailable_GeoLite2-City.mmdb", + "_geoip_database_unavailable_GeoLite2-ASN.mmdb" + ], + "target": { + "entity": { + "id": "projects/your-project-id/roles/customRole" + } + }, + "user_agent": { + "device": { + "name": "Other" + }, + "name": "Other", + "original": "google-cloud-sdk/324.0.0" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "838ea37ab43ab7d2754d007fbe8191be53d7d637bea62f6189f8db1503c0e250", + "index": ".internal.alerts-security.alerts-default-000001", + "source": { + "@timestamp": "2024-09-01T20:39:03.646Z", + "actor": { + "entity": { + "id": "admin@example.com" + } + }, + "client": { + "user": { + "email": "admin@example.com" + } + }, + "cloud": { + "project": { + "id": "your-project-id" + }, + "provider": "gcp" + }, + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "google.iam.admin.v1.CreateRole", + "agent_id_status": "missing", + "category": [ + "session", + "network", + "configuration" + ], + "dataset": "gcp.audit", + "id": "kabcd1234efgh5678", + "ingested": "2024-09-01T20:38:13Z", + "module": "gcp", + "outcome": "success", + "provider": "activity", + "type": [ + "end", + "access", + "allowed" + ] + }, + "event.kind": "signal", + "gcp": { + "audit": { + "authorization_info": [ + { + "granted": true, + "permission": "iam.roles.create", + "resource": "projects/your-project-id" + } + ], + "logentry_operation": { + "id": "operation-0987654321" + }, + "request": { + "@type": "type.googleapis.com/google.iam.admin.v1.CreateRoleRequest", + "parent": "projects/your-project-id", + "role": { + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "title": "Custom Role" + }, + "roleId": "customRole" + }, + "resource_name": "projects/your-project-id/roles/customRole", + "response": { + "@type": "type.googleapis.com/google.iam.admin.v1.Role", + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "stage": "GA", + "title": "Custom Role" + }, + "type": "type.googleapis.com/google.cloud.audit.AuditLog" + } + }, + "kibana.alert.ancestors": [ + { + "depth": 0, + "id": "rhKah5IBGYRrfvcTXtWe", + "index": ".ds-logs-gcp.audit-default-2024.10.13-000001", + "type": "event" + } + ], + "kibana.alert.depth": 1, + "kibana.alert.intended_timestamp": "2024-09-01T20:39:03.657Z", + "kibana.alert.last_detected": "2024-09-01T20:39:03.657Z", + "kibana.alert.original_event.action": "google.iam.admin.v1.CreateRole", + "kibana.alert.original_event.agent_id_status": "missing", + "kibana.alert.original_event.category": [ + "session", + "network", + "configuration" + ], + "kibana.alert.original_event.dataset": "gcp.audit", + "kibana.alert.original_event.id": "kabcd1234efgh5678", + "kibana.alert.original_event.ingested": "2024-09-01T20:38:13Z", + "kibana.alert.original_event.kind": "event", + "kibana.alert.original_event.module": "gcp", + "kibana.alert.original_event.outcome": "success", + "kibana.alert.original_event.provider": "activity", + "kibana.alert.original_event.type": [ + "end", + "access", + "allowed" + ], + "kibana.alert.original_time": "2024-09-01T12:34:56.789Z", + "kibana.alert.reason": "session, network, configuration event with source 10.0.0.1 created medium alert GCP IAM Custom Role Creation.", + "kibana.alert.risk_score": 47, + "kibana.alert.rule.actions": [ + ], + "kibana.alert.rule.author": [ + "Elastic" + ], + "kibana.alert.rule.category": "Custom Query Rule", + "kibana.alert.rule.consumer": "siem", + "kibana.alert.rule.created_at": "2024-09-01T20:38:49.650Z", + "kibana.alert.rule.created_by": "elastic", + "kibana.alert.rule.description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.", + "kibana.alert.rule.enabled": true, + "kibana.alert.rule.exceptions_list": [ + ], + "kibana.alert.rule.execution.timestamp": "2024-09-01T20:39:03.657Z", + "kibana.alert.rule.execution.uuid": "939d34e1-1e74-480d-90ae-24079d9b40d3", + "kibana.alert.rule.false_positives": [ + "Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "kibana.alert.rule.from": "now-6m", + "kibana.alert.rule.immutable": true, + "kibana.alert.rule.indices": [ + "filebeat-*", + "logs-gcp*" + ], + "kibana.alert.rule.interval": "5m", + "kibana.alert.rule.license": "Elastic License v2", + "kibana.alert.rule.max_signals": 100, + "kibana.alert.rule.name": "GCP IAM Custom Role Creation", + "kibana.alert.rule.note": "", + "kibana.alert.rule.parameters": { + "author": [ + "Elastic" + ], + "description": "Identifies an Identity and Access Management (IAM) custom role creation in Google Cloud Platform (GCP). Custom roles are user-defined, and allow for the bundling of one or more supported permissions to meet specific needs. Custom roles will not be updated automatically and could lead to privilege creep if not carefully scrutinized.", + "exceptions_list": [ + ], + "false_positives": [ + "Custom role creations may be done by a system or network administrator. Verify whether the user email, resource name, and/or hostname should be making changes in your environment. Role creations by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule." + ], + "from": "now-6m", + "immutable": true, + "index": [ + "filebeat-*", + "logs-gcp*" + ], + "language": "kuery", + "license": "Elastic License v2", + "max_signals": 100, + "note": "", + "query": "event.dataset:gcp.audit and event.action:google.iam.admin.v*.CreateRole and event.outcome:success\n", + "references": [ + "https://cloud.google.com/iam/docs/understanding-custom-roles" + ], + "related_integrations": [ + { + "integration": "audit", + "package": "gcp", + "version": "^2.0.0" + } + ], + "required_fields": [ + { + "ecs": true, + "name": "event.action", + "type": "keyword" + }, + { + "ecs": true, + "name": "event.dataset", + "type": "keyword" + }, + { + "ecs": true, + "name": "event.outcome", + "type": "keyword" + } + ], + "risk_score": 47, + "risk_score_mapping": [ + ], + "rule_id": "aa8007f0-d1df-49ef-8520-407857594827", + "rule_source": { + "is_customized": false, + "type": "external" + }, + "setup": "The GCP Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", + "severity": "medium", + "severity_mapping": [ + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "timestamp_override": "event.ingested", + "to": "now", + "type": "query", + "version": 104 + }, + "kibana.alert.rule.producer": "siem", + "kibana.alert.rule.references": [ + "https://cloud.google.com/iam/docs/understanding-custom-roles" + ], + "kibana.alert.rule.revision": 0, + "kibana.alert.rule.risk_score": 47, + "kibana.alert.rule.risk_score_mapping": [ + ], + "kibana.alert.rule.rule_id": "aa8007f0-d1df-49ef-8520-407857594827", + "kibana.alert.rule.rule_type_id": "siem.queryRule", + "kibana.alert.rule.severity": "medium", + "kibana.alert.rule.severity_mapping": [ + ], + "kibana.alert.rule.tags": [ + "Domain: Cloud", + "Data Source: GCP", + "Data Source: Google Cloud Platform", + "Use Case: Identity and Access Audit", + "Tactic: Initial Access" + ], + "kibana.alert.rule.threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + } + ], + "kibana.alert.rule.timestamp_override": "event.ingested", + "kibana.alert.rule.to": "now", + "kibana.alert.rule.type": "query", + "kibana.alert.rule.updated_at": "2024-09-01T20:39:00.099Z", + "kibana.alert.rule.updated_by": "elastic", + "kibana.alert.rule.uuid": "c6f64115-5941-46ef-bfa3-61a4ecb4f3ba", + "kibana.alert.rule.version": 104, + "kibana.alert.severity": "medium", + "kibana.alert.start": "2024-09-01T20:39:03.657Z", + "kibana.alert.status": "active", + "kibana.alert.uuid": "838ea37ab43ab7d2754d007fbe8191be53d7d637bea62f6189f8db1503c0e250", + "kibana.alert.workflow_assignee_ids": [ + ], + "kibana.alert.workflow_status": "open", + "kibana.alert.workflow_tags": [ + ], + "kibana.space_ids": [ + "default" + ], + "kibana.version": "9.0.0", + "log": { + "level": "NOTICE", + "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" + }, + "related": { + "ip": [ + "10.0.0.1" + ], + "user": [ + "admin@example.com" + ] + }, + "service": { + "name": "iam.googleapis.com" + }, + "source": { + "ip": "10.0.0.1" + }, + "tags": [ + "_geoip_database_unavailable_GeoLite2-City.mmdb", + "_geoip_database_unavailable_GeoLite2-ASN.mmdb" + ], + "user_agent": { + "device": { + "name": "Other" + }, + "name": "Other", + "original": "google-cloud-sdk/324.0.0" + } + } + } +} diff --git a/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json.gz b/x-pack/test/cloud_security_posture_functional/es_archives/security_alerts/data.json.gz deleted file mode 100644 index 93b2c20b81c86a7cd2edc5006f73c3668bf404a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3143 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 VLPjs>Eq*^?iI?8NxVx~h{0rLzLWckV 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 023d4d1436b22..619990d8e8281 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 @@ -35,8 +35,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // Setting the timerange to fit the data and open the flyout for a specific alert await alertsPage.navigateToAlertsPage( `${alertsPage.getAbsoluteTimerangeFilter( - '2024-10-13T00:00:00.000Z', - '2024-10-14T00:00:00.000Z' + '2024-09-01T00:00:00.000Z', + '2024-09-02T00:00:00.000Z' )}&${alertsPage.getFlyoutFilter( '589e086d7ceec7d4b353340578bd607e96fbac7eab9e2926f110990be15122f1' )}`