>) => {
+ 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'
)}`