[Cloud Security] [Graph Vis] Implement UI enhancements (#222830)

## Summary

Closes:
- https://github.com/elastic/kibana/issues/222367

Enhance graph visualization UI with latest Figma update.

### Screenshots

<details><summary>Popover</summary>
<img width="370" alt="Screenshot 2025-06-12 at 10 21 50"
src="https://github.com/user-attachments/assets/8060860e-5b93-4d71-b330-1920afb75c6a"
/>
</details> 

<details><summary>Controls</summary>
<img width="63" alt="Screenshot 2025-06-11 at 17 36 27"
src="https://github.com/user-attachments/assets/78b93528-1821-4d77-9536-eb88fd68e3dc"
/>
</details>

<details><summary>Edges - new color, default to solid stroke, no start
marker</summary>
<img width="1078" alt="Screenshot 2025-06-12 at 17 22 25"
src="https://github.com/user-attachments/assets/53b46adb-2b79-4c65-ba48-9c74826f2fb0"
/>
</details>

### Videos

#### Snap nodes to 10px grid


https://github.com/user-attachments/assets/fc732784-1e3b-4277-9bf3-d7a6c9b43f88

#### Zoom / Fit to view transition


https://github.com/user-attachments/assets/8a7627c0-7c00-4321-a05c-ea9fa1910002

### Definition of done

- [x] Update popover container
- [x] Update popover action icons
- [x] Update popover action texts
- [x] Update fit-to-view icons
- [x] Update relationship arrow colors
- [x] Check if we can increase the current nodes limit - increased to
300
- [x] Snap to Grid: Enable nodes to snap by 10px
- [x] Implement smooth zoom and fit-to-view transitions (200ms duration)
- [x] Remove edge's start marker
- [x] Remove failure representation according to `event.outcome`

### How to test - In Kibana

1. Add this line to your `kibana.dev.yml`:

    ```yml
    uiSettings.overrides.securitySolution:enableGraphVisualization: true
    ```

2. Then, run these 2 commands while running Kibana with `yarn start
--no-base-path`. This is for setting up the local env with data.

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

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

3. Finally in Kibana, go to Alerts and update the date-picker to include
data from a year ago. Then check one of the alerts details opening the
right-side flyout and find the "Graph preview" section in it.

### How to test - In Storybook

1. Run in terminal:

    ```bash
    yarn storybook cloud_security_posture_graph
    ```

2. Open [http://localhost:9001/](http://localhost:9001/).

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

No risk, feature is gated under the
`securitySolution:enableGraphVisualization` UI setting.

---------

Co-authored-by: Kfir Peled <61654899+kfirpeled@users.noreply.github.com>
Co-authored-by: Kfir Peled <kfir.peled@elastic.co>
This commit is contained in:
Alberto Blázquez 2025-06-24 06:46:38 +02:00 committed by GitHub
parent 1d6a439448
commit 9b5cf91062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 452 additions and 268 deletions

View file

@ -42,12 +42,19 @@ export const graphResponseSchema = () =>
), ),
}); });
export const colorSchema = schema.oneOf([ export const nodeColorSchema = schema.oneOf([
schema.literal('primary'), schema.literal('primary'),
schema.literal('danger'), schema.literal('danger'),
schema.literal('warning'), schema.literal('warning'),
]); ]);
export const edgeColorSchema = schema.oneOf([
schema.literal('primary'),
schema.literal('danger'),
schema.literal('warning'),
schema.literal('subdued'),
]);
export const nodeShapeSchema = schema.oneOf([ export const nodeShapeSchema = schema.oneOf([
schema.literal('hexagon'), schema.literal('hexagon'),
schema.literal('pentagon'), schema.literal('pentagon'),
@ -67,7 +74,7 @@ export const nodeBaseDataSchema = schema.object({
export const entityNodeDataSchema = schema.allOf([ export const entityNodeDataSchema = schema.allOf([
nodeBaseDataSchema, nodeBaseDataSchema,
schema.object({ schema.object({
color: colorSchema, color: nodeColorSchema,
shape: schema.oneOf([ shape: schema.oneOf([
schema.literal('hexagon'), schema.literal('hexagon'),
schema.literal('pentagon'), schema.literal('pentagon'),
@ -90,7 +97,7 @@ export const labelNodeDataSchema = schema.allOf([
schema.object({ schema.object({
shape: schema.literal('label'), shape: schema.literal('label'),
parentId: schema.maybe(schema.string()), parentId: schema.maybe(schema.string()),
color: colorSchema, color: nodeColorSchema,
}), }),
]); ]);
@ -98,6 +105,6 @@ export const edgeDataSchema = schema.object({
id: schema.string(), id: schema.string(),
source: schema.string(), source: schema.string(),
target: schema.string(), target: schema.string(),
color: colorSchema, color: edgeColorSchema,
type: schema.maybe(schema.oneOf([schema.literal('solid'), schema.literal('dashed')])), type: schema.maybe(schema.oneOf([schema.literal('solid'), schema.literal('dashed')])),
}); });

View file

@ -8,13 +8,14 @@
import type { TypeOf } from '@kbn/config-schema'; import type { TypeOf } from '@kbn/config-schema';
import type { BoolQuery } from '@kbn/es-query'; import type { BoolQuery } from '@kbn/es-query';
import { import {
colorSchema, edgeColorSchema,
edgeDataSchema, edgeDataSchema,
entityNodeDataSchema, entityNodeDataSchema,
graphRequestSchema, graphRequestSchema,
graphResponseSchema, graphResponseSchema,
groupNodeDataSchema, groupNodeDataSchema,
labelNodeDataSchema, labelNodeDataSchema,
nodeColorSchema,
nodeShapeSchema, nodeShapeSchema,
} from '../../schema/graph/v1'; } from '../../schema/graph/v1';
@ -25,7 +26,8 @@ export type GraphResponse = Omit<TypeOf<typeof graphResponseSchema>, 'messages'>
messages?: ApiMessageCode[]; messages?: ApiMessageCode[];
}; };
export type Color = typeof colorSchema.type; export type EdgeColor = typeof edgeColorSchema.type;
export type NodeColor = typeof nodeColorSchema.type;
export type NodeShape = TypeOf<typeof nodeShapeSchema>; export type NodeShape = TypeOf<typeof nodeShapeSchema>;

View file

@ -7489,14 +7489,6 @@
"securitySolutionPackages.csp.graph.controls.toggleSearchBar.tour.title": "Affinez votre vue avec la recherche", "securitySolutionPackages.csp.graph.controls.toggleSearchBar.tour.title": "Affinez votre vue avec la recherche",
"securitySolutionPackages.csp.graph.controls.zoomIn": "Zoom avant", "securitySolutionPackages.csp.graph.controls.zoomIn": "Zoom avant",
"securitySolutionPackages.csp.graph.controls.zoomOut": "Zoom arrière", "securitySolutionPackages.csp.graph.controls.zoomOut": "Zoom arrière",
"securitySolutionPackages.csp.graph.graphLabelExpandPopover.hideEventsWithThisAction": "Masquer les événements avec cette action",
"securitySolutionPackages.csp.graph.graphLabelExpandPopover.showEventsWithThisAction": "Afficher les événements avec cette action",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsByEntity": "Masquer les actions par cette entité",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsOnEntity": "Masquer les actions sur cette entité",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideRelatedEntities": "Masquer les entités liées",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsByEntity": "Afficher les actions par cette entité",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsOnEntity": "Afficher les actions sur cette entité",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showRelatedEntities": "Afficher les entités liées",
"securitySolutionPackages.csp.graph.investigation.errorBuildingQuery": "Impossible d'extraire les résultats de recherche", "securitySolutionPackages.csp.graph.investigation.errorBuildingQuery": "Impossible d'extraire les résultats de recherche",
"securitySolutionPackages.csp.graph.investigation.warningNegatedFilterContent": "Un ou plusieurs filtres sont annulés et ne renverront donc peut-être pas les résultats attendus.", "securitySolutionPackages.csp.graph.investigation.warningNegatedFilterContent": "Un ou plusieurs filtres sont annulés et ne renverront donc peut-être pas les résultats attendus.",
"securitySolutionPackages.csp.graph.investigation.warningNegatedFilterTitle": "Filtres annulés", "securitySolutionPackages.csp.graph.investigation.warningNegatedFilterTitle": "Filtres annulés",

View file

@ -7489,14 +7489,6 @@
"securitySolutionPackages.csp.graph.controls.toggleSearchBar.tour.title": "検索でビューを絞り込む", "securitySolutionPackages.csp.graph.controls.toggleSearchBar.tour.title": "検索でビューを絞り込む",
"securitySolutionPackages.csp.graph.controls.zoomIn": "ズームイン", "securitySolutionPackages.csp.graph.controls.zoomIn": "ズームイン",
"securitySolutionPackages.csp.graph.controls.zoomOut": "ズームアウト", "securitySolutionPackages.csp.graph.controls.zoomOut": "ズームアウト",
"securitySolutionPackages.csp.graph.graphLabelExpandPopover.hideEventsWithThisAction": "このアクションのイベントを非表示",
"securitySolutionPackages.csp.graph.graphLabelExpandPopover.showEventsWithThisAction": "このアクションのイベントを表示",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsByEntity": "このエンティティによるアクションを非表示",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsOnEntity": "このエンティティに対するアクションを非表示",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideRelatedEntities": "関連するエンティティを非表示",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsByEntity": "このエンティティによるアクションを表示",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsOnEntity": "このエンティティに対するアクションを表示",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showRelatedEntities": "関連するエンティティを表示",
"securitySolutionPackages.csp.graph.investigation.errorBuildingQuery": "検索結果を取得できません", "securitySolutionPackages.csp.graph.investigation.errorBuildingQuery": "検索結果を取得できません",
"securitySolutionPackages.csp.graph.investigation.warningNegatedFilterContent": "1つ以上のフィルターが否定され、意図した結果が返されない場合があります。", "securitySolutionPackages.csp.graph.investigation.warningNegatedFilterContent": "1つ以上のフィルターが否定され、意図した結果が返されない場合があります。",
"securitySolutionPackages.csp.graph.investigation.warningNegatedFilterTitle": "フィルターが否定されました", "securitySolutionPackages.csp.graph.investigation.warningNegatedFilterTitle": "フィルターが否定されました",

View file

@ -7484,14 +7484,6 @@
"securitySolutionPackages.csp.graph.controls.toggleSearchBar.tour.title": "通过搜索优化您的视图", "securitySolutionPackages.csp.graph.controls.toggleSearchBar.tour.title": "通过搜索优化您的视图",
"securitySolutionPackages.csp.graph.controls.zoomIn": "放大", "securitySolutionPackages.csp.graph.controls.zoomIn": "放大",
"securitySolutionPackages.csp.graph.controls.zoomOut": "缩小", "securitySolutionPackages.csp.graph.controls.zoomOut": "缩小",
"securitySolutionPackages.csp.graph.graphLabelExpandPopover.hideEventsWithThisAction": "通过此操作隐藏事件",
"securitySolutionPackages.csp.graph.graphLabelExpandPopover.showEventsWithThisAction": "通过此操作显示事件",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsByEntity": "隐藏此实体执行的操作",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsOnEntity": "隐藏对此实体执行的操作",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideRelatedEntities": "隐藏相关实体",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsByEntity": "显示此实体执行的操作",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsOnEntity": "显示对此实体执行的操作",
"securitySolutionPackages.csp.graph.graphNodeExpandPopover.showRelatedEntities": "显示相关实体",
"securitySolutionPackages.csp.graph.investigation.errorBuildingQuery": "无法检索搜索结果", "securitySolutionPackages.csp.graph.investigation.errorBuildingQuery": "无法检索搜索结果",
"securitySolutionPackages.csp.graph.investigation.warningNegatedFilterContent": "一个或多个筛选已作废,可能无法返回预期结果。", "securitySolutionPackages.csp.graph.investigation.warningNegatedFilterContent": "一个或多个筛选已作废,可能无法返回预期结果。",
"securitySolutionPackages.csp.graph.investigation.warningNegatedFilterTitle": "筛选已作废", "securitySolutionPackages.csp.graph.investigation.warningNegatedFilterTitle": "筛选已作废",

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5393 11.7311V4.26895L6.29822 5.63447C6.11561 5.82722 5.81956 5.82722 5.63695 5.63447C5.45435 5.44173 5.45435 5.12923 5.63695 4.93649L7.0081 3.43367C7.55591 2.85544 8.44409 2.85544 8.9919 3.43367L10.363 4.93649C10.5457 5.12923 10.5457 5.44173 10.363 5.63447C10.1804 5.82722 9.88439 5.82722 9.70178 5.63447L8.47447 4.28348V11.7165L9.70178 10.3655C9.88439 10.1728 10.1804 10.1728 10.363 10.3655C10.5457 10.5583 10.5457 10.8708 10.363 11.0635L8.9919 12.5663C8.44409 13.1446 7.55591 13.1446 7.0081 12.5663L5.63695 11.0635C5.45435 10.8708 5.45435 10.5583 5.63695 10.3655C5.81956 10.1728 6.11561 10.1728 6.29822 10.3655L7.5393 11.7311Z" fill="#1D2A3E"/>
<path d="M12.7311 8.4607H3.26895L4.63447 9.70178C4.82722 9.88439 4.82722 10.1804 4.63447 10.363C4.44173 10.5457 4.12923 10.5457 3.93649 10.363L2.43367 8.9919C1.85544 8.44409 1.85544 7.55591 2.43367 7.0081L3.93649 5.63695C4.12923 5.45435 4.44173 5.45435 4.63447 5.63695C4.82722 5.81956 4.82722 6.11561 4.63447 6.29822L3.28348 7.52553H12.7165L11.3655 6.29822C11.1728 6.11561 11.1728 5.81956 11.3655 5.63695C11.5583 5.45435 11.8708 5.45435 12.0635 5.63695L13.5663 7.0081C14.1446 7.55591 14.1446 8.44409 13.5663 8.9919L12.0635 10.363C11.8708 10.5457 11.5583 10.5457 11.3655 10.363C11.1728 10.1804 11.1728 9.88439 11.3655 9.70178L12.7311 8.4607Z" fill="#1D2A3E"/>
<path d="M14 1C15.1046 1 16 1.89543 16 3V13L15.9893 13.2041C15.887 14.2128 15.0357 15 14 15H2L1.7959 14.9893C0.854346 14.8938 0.1062 14.1457 0.0107422 13.2041L0 13V3C0 1.96435 0.787223 1.113 1.7959 1.01074L2 1H14ZM2 2C1.44772 2 1 2.44772 1 3V13C1 13.5523 1.44772 14 2 14H14C14.5523 14 15 13.5523 15 13V3C15 2.44772 14.5523 2 14 2H2Z" fill="#1D2A3E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -18,4 +18,4 @@ export const SHOW_SEARCH_BAR_BUTTON_TOUR_STORAGE_KEY =
export const TOGGLE_SEARCH_BAR_STORAGE_KEY = export const TOGGLE_SEARCH_BAR_STORAGE_KEY =
'securitySolution.graphInvestigation:toggleSearchBarState' as const; 'securitySolution.graphInvestigation:toggleSearchBarState' as const;
export const GRAPH_NODES_LIMIT = 100; export const GRAPH_NODES_LIMIT = 300;

View file

@ -133,7 +133,7 @@ export const Actions = ({
: undefined; : undefined;
return ( return (
<EuiFlexGroup direction="column" gutterSize={'none'} {...props}> <EuiFlexGroup direction="column" gutterSize="none" {...props}>
{showToggleSearch && ( {showToggleSearch && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiTourStep <EuiTourStep
@ -154,6 +154,7 @@ export const Actions = ({
css={[ css={[
css` css`
position: relative; position: relative;
overflow: visible;
width: 40px; width: 40px;
`, `,
!searchToggled !searchToggled
@ -163,6 +164,11 @@ export const Actions = ({
` `
: undefined, : undefined,
]} ]}
contentProps={{
css: css`
position: initial;
`,
}}
minWidth={false} minWidth={false}
size="m" size="m"
aria-label={toggleSearchBarTooltip} aria-label={toggleSearchBarTooltip}
@ -209,7 +215,11 @@ export const Actions = ({
</EuiTourStep> </EuiTourStep>
</EuiFlexItem> </EuiFlexItem>
)} )}
{showToggleSearch && showInvestigateInTimeline && <EuiHorizontalRule margin="xs" />} {showToggleSearch && showInvestigateInTimeline && (
<EuiFlexItem grow={false}>
<EuiHorizontalRule margin="xs" />
</EuiFlexItem>
)}
{showInvestigateInTimeline && ( {showInvestigateInTimeline && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiToolTip content={investigateInTimelineTooltip} position="left"> <EuiToolTip content={investigateInTimelineTooltip} position="left">

View file

@ -10,7 +10,8 @@ import {
EuiButtonIcon, EuiButtonIcon,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiSpacer, EuiHorizontalRule,
EuiIcon,
useEuiTheme, useEuiTheme,
type CommonProps, type CommonProps,
} from '@elastic/eui'; } from '@elastic/eui';
@ -23,6 +24,7 @@ import {
GRAPH_CONTROLS_ZOOM_IN_ID, GRAPH_CONTROLS_ZOOM_IN_ID,
GRAPH_CONTROLS_ZOOM_OUT_ID, GRAPH_CONTROLS_ZOOM_OUT_ID,
} from '../test_ids'; } from '../test_ids';
import fitToViewIcon from '../../assets/icons/fit_to_view.svg';
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
minZoomReached: s.transform[2] <= s.minZoom, minZoomReached: s.transform[2] <= s.minZoom,
@ -57,6 +59,8 @@ const CenterLabel = i18n.translate('securitySolutionPackages.csp.graph.controls.
defaultMessage: 'Center', defaultMessage: 'Center',
}); });
const fitToViewIconFn = () => <EuiIcon type={fitToViewIcon} size="m" color="text" />;
export const Controls = ({ export const Controls = ({
showZoom = true, showZoom = true,
showFitView = true, showFitView = true,
@ -73,12 +77,12 @@ export const Controls = ({
const { maxZoomReached, minZoomReached } = useStore(selector); const { maxZoomReached, minZoomReached } = useStore(selector);
const onZoomInHandler = () => { const onZoomInHandler = () => {
zoomIn(); zoomIn({ duration: fitViewOptions?.duration });
onZoomIn?.(); onZoomIn?.();
}; };
const onZoomOutHandler = () => { const onZoomOutHandler = () => {
zoomOut(); zoomOut({ duration: fitViewOptions?.duration });
onZoomOut?.(); onZoomOut?.();
}; };
@ -88,10 +92,13 @@ export const Controls = ({
}; };
const btnCss = css` const btnCss = css`
border-radius: 0;
`;
const groupCss = css`
border: ${euiTheme.border.thin}; border: ${euiTheme.border.thin};
border-radius: ${euiTheme.border.radius.medium}; border-radius: ${euiTheme.border.radius.medium};
background-color: ${euiTheme.colors.backgroundBasePlain}; background-color: ${euiTheme.colors.backgroundBasePlain};
box-sizing: content-box;
`; `;
if (!showZoom && !showCenter && !showFitView) { if (!showZoom && !showCenter && !showFitView) {
@ -99,9 +106,10 @@ export const Controls = ({
} }
return ( return (
<EuiFlexGroup direction="column" gutterSize={'none'} {...props}> <EuiFlexGroup direction="column" gutterSize="none" css={groupCss} {...props}>
{showZoom && ( {showZoom && (
<EuiFlexItem grow={false} css={btnCss}> <>
<EuiFlexItem grow={false}>
<EuiButtonIcon <EuiButtonIcon
iconType="plusInCircle" iconType="plusInCircle"
aria-label={ZoomInLabel} aria-label={ZoomInLabel}
@ -109,8 +117,11 @@ export const Controls = ({
color="text" color="text"
data-test-subj={GRAPH_CONTROLS_ZOOM_IN_ID} data-test-subj={GRAPH_CONTROLS_ZOOM_IN_ID}
disabled={maxZoomReached} disabled={maxZoomReached}
css={btnCss}
onClick={onZoomInHandler} onClick={onZoomInHandler}
/> />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon <EuiButtonIcon
iconType="minusInCircle" iconType="minusInCircle"
aria-label={ZoomOutLabel} aria-label={ZoomOutLabel}
@ -118,12 +129,15 @@ export const Controls = ({
color="text" color="text"
data-test-subj={GRAPH_CONTROLS_ZOOM_OUT_ID} data-test-subj={GRAPH_CONTROLS_ZOOM_OUT_ID}
disabled={minZoomReached} disabled={minZoomReached}
css={btnCss}
onClick={onZoomOutHandler} onClick={onZoomOutHandler}
/> />
</EuiFlexItem> </EuiFlexItem>
</>
)} )}
{showZoom && showCenter && <EuiSpacer size="xs" />}
{showCenter && ( {showCenter && (
<EuiFlexItem grow={false}>
{showZoom ? <EuiHorizontalRule size="full" margin="none" /> : null}
<EuiButtonIcon <EuiButtonIcon
iconType="bullseye" iconType="bullseye"
aria-label={CenterLabel} aria-label={CenterLabel}
@ -133,12 +147,13 @@ export const Controls = ({
css={btnCss} css={btnCss}
onClick={() => onCenter?.()} onClick={() => onCenter?.()}
/> />
</EuiFlexItem>
)} )}
{(showZoom || showCenter) && showFitView && <EuiSpacer size="xs" />}
{showFitView && ( {showFitView && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
{showZoom || showCenter ? <EuiHorizontalRule size="full" margin="none" /> : null}
<EuiButtonIcon <EuiButtonIcon
iconType="continuityWithin" iconType={fitToViewIconFn}
aria-label={FitViewLabel} aria-label={FitViewLabel}
size="m" size="m"
color="text" color="text"

View file

@ -64,6 +64,7 @@ const edgeTypes = {
const Template = (args: EdgeViewModel) => { const Template = (args: EdgeViewModel) => {
const isArrayOfObjectsEqual = (x: object[], y: object[]) => const isArrayOfObjectsEqual = (x: object[], y: object[]) =>
size(x) === size(y) && isEmpty(xorWith(x, y, isEqual)); size(x) === size(y) && isEmpty(xorWith(x, y, isEqual));
const edgeData = pick(args, ['id', 'label', 'interactive', 'source', 'target', 'color', 'type']);
const nodes = useMemo( const nodes = useMemo(
() => [ () => [
@ -86,11 +87,14 @@ const Template = (args: EdgeViewModel) => {
{ {
id: args.id, id: args.id,
type: 'label', type: 'label',
data: pick(args, ['id', 'label', 'interactive', 'source', 'target', 'color', 'type']), data: {
...edgeData,
color: edgeData.color === 'subdued' ? 'primary' : edgeData.color,
},
position: { x: 230, y: 6 }, position: { x: 230, y: 6 },
}, },
], ],
[args] [args, edgeData]
); );
const edges = useMemo( const edges = useMemo(
@ -172,7 +176,7 @@ export default {
render: Template, render: Template,
argTypes: { argTypes: {
color: { color: {
options: ['primary', 'danger', 'warning'], options: ['primary', 'danger', 'warning', 'subdued'],
control: { type: 'radio' }, control: { type: 'radio' },
}, },
type: { type: {

View file

@ -7,10 +7,10 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { BaseEdge, getSmoothStepPath } from '@xyflow/react'; import { BaseEdge, getSmoothStepPath } from '@xyflow/react';
import { useEuiTheme } from '@elastic/eui';
import type { EdgeProps, EdgeViewModel } from '../types'; import type { EdgeProps, EdgeViewModel } from '../types';
import { getShapeHandlePosition } from './utils'; import { getShapeHandlePosition } from './utils';
import { getMarkerStart, getMarkerEnd } from './markers'; import { getMarkerEnd } from './markers';
import { useEdgeColor } from './styles';
type EdgeColor = EdgeViewModel['color']; type EdgeColor = EdgeViewModel['color'];
@ -32,14 +32,9 @@ export const DefaultEdge = memo(
targetPosition, targetPosition,
data, data,
}: EdgeProps) => { }: EdgeProps) => {
const { euiTheme } = useEuiTheme();
const color: EdgeColor = data?.color || 'primary'; const color: EdgeColor = data?.color || 'primary';
const sourceMargin = getShapeHandlePosition(data?.sourceShape); const sourceMargin = getShapeHandlePosition(data?.sourceShape);
const targetMargin = getShapeHandlePosition(data?.targetShape); const targetMargin = getShapeHandlePosition(data?.targetShape);
const markerStart =
!data?.sourceShape || !NODES_WITHOUT_MARKER.includes(data?.sourceShape)
? getMarkerStart(color)
: undefined;
const markerEnd = const markerEnd =
!data?.targetShape || !NODES_WITHOUT_MARKER.includes(data?.targetShape) !data?.targetShape || !NODES_WITHOUT_MARKER.includes(data?.targetShape)
? getMarkerEnd(color) ? getMarkerEnd(color)
@ -69,11 +64,10 @@ export const DefaultEdge = memo(
path={edgePath} path={edgePath}
interactionWidth={0} interactionWidth={0}
style={{ style={{
stroke: euiTheme.colors[color], stroke: useEdgeColor(color),
// Defaults to dashed when type is not available // Defaults to solid when type is not available
...(!data?.type || data?.type === 'dashed' ? dashedStyle : {}), ...(data?.type === 'dashed' ? dashedStyle : {}),
}} }}
markerStart={markerStart}
markerEnd={markerEnd} markerEnd={markerEnd}
/> />
</> </>

View file

@ -6,7 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import { useEuiTheme } from '@elastic/eui'; import { useEdgeColor } from './styles';
const getArrowPoints = (width: number, height: number): string => { const getArrowPoints = (width: number, height: number): string => {
return `${-width},${-height} 0,0 ${-width},${height} ${-width},${-height}`; return `${-width},${-height} 0,0 ${-width},${height} ${-width},${-height}`;
@ -48,29 +48,11 @@ const ArrowMarker = ({
); );
}; };
const DotMarker = ({ id, color }: { id: string; color: string }) => {
return (
<marker id={id} markerWidth="6" markerHeight="6" refX="0.1" refY="3" orient="auto">
<circle cx="3" cy="3" r="3" fill={color} />
</marker>
);
};
const MarkerStartType = {
primary: 'url(#dotPrimary)',
danger: 'url(#dotDanger)',
warning: 'url(#dotWarning)',
};
const MarkerEndType = { const MarkerEndType = {
primary: 'url(#arrowPrimary)', primary: 'url(#arrowPrimary)',
danger: 'url(#arrowDanger)', subdued: 'url(#arrowSubdued)',
warning: 'url(#arrowWarning)', warning: 'url(#arrowWarning)',
}; danger: 'url(#arrowDanger)',
export const getMarkerStart = (color: string) => {
const colorKey = color as keyof typeof MarkerStartType;
return MarkerStartType[colorKey] ?? MarkerStartType.primary;
}; };
export const getMarkerEnd = (color: string) => { export const getMarkerEnd = (color: string) => {
@ -79,17 +61,13 @@ export const getMarkerEnd = (color: string) => {
}; };
export const SvgDefsMarker = () => { export const SvgDefsMarker = () => {
const { euiTheme } = useEuiTheme();
return ( return (
<svg css={{ position: 'absolute', width: 0, height: 0 }}> <svg css={{ position: 'absolute', width: 0, height: 0 }}>
<defs> <defs>
<ArrowMarker id="arrowPrimary" color={euiTheme.colors.primary} width={6} height={4.8} /> <ArrowMarker id="arrowPrimary" color={useEdgeColor('primary')} width={6} height={4.8} />
<ArrowMarker id="arrowDanger" color={euiTheme.colors.danger} width={6} height={4.8} /> <ArrowMarker id="arrowSubdued" color={useEdgeColor('subdued')} width={6} height={4.8} />
<ArrowMarker id="arrowWarning" color={euiTheme.colors.warning} width={6} height={4.8} /> <ArrowMarker id="arrowWarning" color={useEdgeColor('warning')} width={6} height={4.8} />
<DotMarker id="dotPrimary" color={euiTheme.colors.primary} /> <ArrowMarker id="arrowDanger" color={useEdgeColor('danger')} width={6} height={4.8} />
<DotMarker id="dotDanger" color={euiTheme.colors.danger} />
<DotMarker id="dotWarning" color={euiTheme.colors.warning} />
</defs> </defs>
</svg> </svg>
); );

View file

@ -14,6 +14,7 @@ import {
type EuiTextProps, type EuiTextProps,
type _EuiBackgroundColor, type _EuiBackgroundColor,
} from '@elastic/eui'; } from '@elastic/eui';
import type { EdgeViewModel } from '../types';
export const EdgeLabelHeight = 24; export const EdgeLabelHeight = 24;
export const EdgeLabelWidth = 100; export const EdgeLabelWidth = 100;
@ -87,3 +88,18 @@ export const EdgeLabelOnHover = styled(EdgeLabel)<EdgeLabelProps & EdgeLabelCont
opacity: 1; /* Show on hover */ opacity: 1; /* Show on hover */
} }
`; `;
export const useEdgeColor = (edgeColor: EdgeViewModel['color']) => {
const { euiTheme } = useEuiTheme();
switch (edgeColor) {
case 'danger':
return euiTheme.colors.danger;
case 'warning':
return euiTheme.colors.warning;
case 'primary':
return euiTheme.colors.primary;
case 'subdued':
default:
return euiTheme.colors.borderBaseFormsControl;
}
};

View file

@ -9,3 +9,5 @@
* Whether or not to instruct the graph component to only render nodes and edges that would be visible in the viewport. * Whether or not to instruct the graph component to only render nodes and edges that would be visible in the viewport.
*/ */
export const ONLY_RENDER_VISIBLE_ELEMENTS = true as const; export const ONLY_RENDER_VISIBLE_ELEMENTS = true as const;
export const GRID_SIZE = 10; // in px

View file

@ -31,7 +31,7 @@ import {
import { layoutGraph } from './layout_graph'; import { layoutGraph } from './layout_graph';
import { DefaultEdge } from '../edge'; import { DefaultEdge } from '../edge';
import type { EdgeViewModel, NodeViewModel } from '../types'; import type { EdgeViewModel, NodeViewModel } from '../types';
import { ONLY_RENDER_VISIBLE_ELEMENTS } from './constants'; import { ONLY_RENDER_VISIBLE_ELEMENTS, GRID_SIZE } from './constants';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import { Controls } from '../controls/controls'; import { Controls } from '../controls/controls';
@ -74,6 +74,10 @@ const edgeTypes = {
default: DefaultEdge, default: DefaultEdge,
}; };
const fitViewOptions: FitViewOptions<Node> = {
duration: 200,
};
/** /**
* Graph component renders a graph visualization using ReactFlow. * Graph component renders a graph visualization using ReactFlow.
* It takes nodes and edges as input and provides interactive controls * It takes nodes and edges as input and provides interactive controls
@ -114,7 +118,7 @@ export const Graph = memo<GraphProps>(
currNodesRef.current = nodes; currNodesRef.current = nodes;
currEdgesRef.current = edges; currEdgesRef.current = edges;
setTimeout(() => { setTimeout(() => {
fitViewRef.current?.(); fitViewRef.current?.(fitViewOptions);
}, 30); }, 30);
} }
}, [nodes, edges, setNodes, setEdges, isGraphInteractive]); }, [nodes, edges, setNodes, setEdges, isGraphInteractive]);
@ -150,7 +154,7 @@ export const Graph = memo<GraphProps>(
edgesFocusable={false} edgesFocusable={false}
onlyRenderVisibleElements={ONLY_RENDER_VISIBLE_ELEMENTS} onlyRenderVisibleElements={ONLY_RENDER_VISIBLE_ELEMENTS}
snapToGrid={true} // Snap to grid is enabled to avoid sub-pixel positioning snapToGrid={true} // Snap to grid is enabled to avoid sub-pixel positioning
snapGrid={[1, 1]} snapGrid={[GRID_SIZE, GRID_SIZE]} // Snap nodes to a 10px grid
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
proOptions={{ hideAttribution: true }} proOptions={{ hideAttribution: true }}
@ -165,7 +169,7 @@ export const Graph = memo<GraphProps>(
> >
{interactive && ( {interactive && (
<Panel position="bottom-right"> <Panel position="bottom-right">
<Controls showCenter={false} /> <Controls fitViewOptions={fitViewOptions} showCenter={false} />
</Panel> </Panel>
)} )}
{children} {children}

View file

@ -7,8 +7,7 @@
import React, { type PropsWithChildren } from 'react'; import React, { type PropsWithChildren } from 'react';
import type { CommonProps, EuiWrappingPopoverProps } from '@elastic/eui'; import type { CommonProps, EuiWrappingPopoverProps } from '@elastic/eui';
import { EuiWrappingPopover, useEuiTheme } from '@elastic/eui'; import { EuiWrappingPopover } from '@elastic/eui';
import { css } from '@emotion/react';
export interface GraphPopoverProps export interface GraphPopoverProps
extends PropsWithChildren, extends PropsWithChildren,
@ -29,8 +28,6 @@ export const GraphPopover = ({
children, children,
...rest ...rest
}: GraphPopoverProps) => { }: GraphPopoverProps) => {
const { euiTheme } = useEuiTheme();
if (!anchorElement) { if (!anchorElement) {
return null; return null;
} }
@ -38,16 +35,6 @@ export const GraphPopover = ({
return ( return (
<EuiWrappingPopover <EuiWrappingPopover
{...rest} {...rest}
panelProps={{
css: css`
.euiPopover__arrow {
--euiPopoverBackgroundColor: ${euiTheme.colors?.body};
}
background-color: ${euiTheme.colors?.body};
`,
}}
color={euiTheme.colors?.body}
isOpen={isOpen} isOpen={isOpen}
closePopover={closePopover} closePopover={closePopover}
button={anchorElement} button={anchorElement}

View file

@ -87,7 +87,7 @@ const showActionsByNode = (container: HTMLElement, nodeId: string) => {
expandNode(container, nodeId); expandNode(container, nodeId);
const btn = screen.getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID); const btn = screen.getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID);
expect(btn).toHaveTextContent('Show actions by this entity'); expect(btn).toHaveTextContent("Show this entity's actions");
btn.click(); btn.click();
}; };
@ -95,7 +95,7 @@ const hideActionsByNode = (container: HTMLElement, nodeId: string) => {
expandNode(container, nodeId); expandNode(container, nodeId);
const hideBtn = screen.getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID); const hideBtn = screen.getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID);
expect(hideBtn).toHaveTextContent('Hide actions by this entity'); expect(hideBtn).toHaveTextContent("Hide this entity's actions");
hideBtn.click(); hideBtn.click();
}; };
@ -254,7 +254,7 @@ describe('GraphInvestigation Component', () => {
expandNode(container, 'admin@example.com'); expandNode(container, 'admin@example.com');
expect(getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID)).toHaveTextContent( expect(getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID)).toHaveTextContent(
'Show actions by this entity' "Show this entity's actions"
); );
}); });
@ -272,7 +272,7 @@ describe('GraphInvestigation Component', () => {
expandNode(container, 'admin@example.com'); expandNode(container, 'admin@example.com');
expect(getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID)).toHaveTextContent( expect(getByTestId(GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID)).toHaveTextContent(
'Show actions by this entity' "Show this entity's actions"
); );
}); });
}); });

View file

@ -68,17 +68,17 @@ export const ListGroupGraphPopover = memo<ListGroupGraphPopoverProps>(
return ( return (
<GraphPopover <GraphPopover
panelPaddingSize="s" panelPaddingSize="none"
anchorPosition="rightCenter" anchorPosition="rightCenter"
isOpen={isOpen} isOpen={isOpen}
anchorElement={anchorElement} anchorElement={anchorElement}
closePopover={closePopover} closePopover={closePopover}
data-test-subj={testSubject} data-test-subj={testSubject}
> >
<EuiListGroup gutterSize="none" bordered={false} flush={true}> <EuiListGroup gutterSize="none" bordered={false} flush={true} size="l">
{listItems.map((item, index) => { {listItems.map((item, index) => {
if (item.type === 'separator') { if (item.type === 'separator') {
return <EuiHorizontalRule key={index} margin="xs" />; return <EuiHorizontalRule key={index} margin="none" size="full" />;
} }
return ( return (
<ExpandPopoverListItem <ExpandPopoverListItem

View file

@ -90,20 +90,20 @@ export const useEntityNodeExpandPopover = (
return [ return [
{ {
type: 'item', type: 'item',
iconType: 'users', iconType: 'sortRight',
testSubject: GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID, testSubject: GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID,
label: label:
actionsByEntityAction === 'show' actionsByEntityAction === 'show'
? i18n.translate( ? i18n.translate(
'securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsByEntity', 'securitySolutionPackages.csp.graph.graphNodeExpandPopover.showThisEntitysActions',
{ {
defaultMessage: 'Show actions by this entity', defaultMessage: "Show this entity's actions",
} }
) )
: i18n.translate( : i18n.translate(
'securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsByEntity', 'securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideThisEntitysActions',
{ {
defaultMessage: 'Hide actions by this entity', defaultMessage: "Hide this entity's actions",
} }
), ),
onClick: () => { onClick: () => {
@ -112,20 +112,20 @@ export const useEntityNodeExpandPopover = (
}, },
{ {
type: 'item', type: 'item',
iconType: 'storage', iconType: 'sortLeft',
testSubject: GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID, testSubject: GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID,
label: label:
actionsOnEntityAction === 'show' actionsOnEntityAction === 'show'
? i18n.translate( ? i18n.translate(
'securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsOnEntity', 'securitySolutionPackages.csp.graph.graphNodeExpandPopover.showActionsDoneToThisEntity',
{ {
defaultMessage: 'Show actions on this entity', defaultMessage: 'Show actions done to this entity',
} }
) )
: i18n.translate( : i18n.translate(
'securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsOnEntity', 'securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideActionsDoneToThisEntity',
{ {
defaultMessage: 'Hide actions on this entity', defaultMessage: 'Hide actions done to this entity',
} }
), ),
onClick: () => { onClick: () => {
@ -134,20 +134,20 @@ export const useEntityNodeExpandPopover = (
}, },
{ {
type: 'item', type: 'item',
iconType: 'visTagCloud', iconType: 'analyzeEvent',
testSubject: GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID, testSubject: GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID,
label: label:
relatedEntitiesAction === 'show' relatedEntitiesAction === 'show'
? i18n.translate( ? i18n.translate(
'securitySolutionPackages.csp.graph.graphNodeExpandPopover.showRelatedEntities', 'securitySolutionPackages.csp.graph.graphNodeExpandPopover.showRelatedEntities',
{ {
defaultMessage: 'Show related entities', defaultMessage: 'Show related events',
} }
) )
: i18n.translate( : i18n.translate(
'securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideRelatedEntities', 'securitySolutionPackages.csp.graph.graphNodeExpandPopover.hideRelatedEntities',
{ {
defaultMessage: 'Hide related entities', defaultMessage: 'Hide related events',
} }
), ),
onClick: () => { onClick: () => {

View file

@ -66,17 +66,17 @@ export const useLabelNodeExpandPopover = (
return [ return [
{ {
type: 'item', type: 'item',
iconType: 'users', iconType: 'analyzeEvent',
testSubject: GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID, testSubject: GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID,
label: label:
eventsWithThisActionToggleAction === 'show' eventsWithThisActionToggleAction === 'show'
? i18n.translate( ? i18n.translate(
'securitySolutionPackages.csp.graph.graphLabelExpandPopover.showEventsWithThisAction', 'securitySolutionPackages.csp.graph.graphLabelExpandPopover.showRelatedEvents',
{ defaultMessage: 'Show events with this action' } { defaultMessage: 'Show related events' }
) )
: i18n.translate( : i18n.translate(
'securitySolutionPackages.csp.graph.graphLabelExpandPopover.hideEventsWithThisAction', 'securitySolutionPackages.csp.graph.graphLabelExpandPopover.hideRelatedEvents',
{ defaultMessage: 'Hide events with this action' } { defaultMessage: 'Hide related events' }
), ),
onClick: () => { onClick: () => {
onShowEventsWithThisActionClick(node, eventsWithThisActionToggleAction); onShowEventsWithThisActionClick(node, eventsWithThisActionToggleAction);

View file

@ -331,6 +331,91 @@ export const GroupWithWarningAPIMock: Story = {
}, },
}; };
export const GroupWithAlertAPIMock: Story = {
args: {
...meta.args,
nodes: [
{
id: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
shape: 'group',
},
{
id: 'admin@example.com',
color: 'primary',
shape: 'ellipse',
icon: 'user',
},
{
id: 'projects/your-project-id/roles/customRole',
color: 'primary',
shape: 'hexagon',
},
{
id: 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)',
label: 'google.iam.admin.v1.CreateRole',
color: 'danger',
shape: 'label',
parentId: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
},
{
id: 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.UpdateRole)',
label: 'google.iam.admin.v1.UpdateRole',
color: 'primary',
shape: 'label',
parentId: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
},
],
edges: [
{
id: 'a(admin@example.com)-b(grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole)))',
source: 'admin@example.com',
target: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
color: 'danger',
type: 'solid',
},
{
id: 'a(grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole)))-b(projects/your-project-id/roles/customRole)',
source: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
target: 'projects/your-project-id/roles/customRole',
color: 'danger',
type: 'solid',
},
{
id: 'a(grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole)))-b(a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole))',
source: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
target:
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)',
color: 'danger',
type: 'solid',
},
{
id: 'a(a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole))-b(grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole)))',
source:
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)',
target: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
color: 'danger',
type: 'solid',
},
{
id: 'a(grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole)))-b(a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.UpdateRole))',
source: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
target:
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.UpdateRole)',
color: 'subdued',
type: 'solid',
},
{
id: 'a(a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.UpdateRole))-b(grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole)))',
source:
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.UpdateRole)',
target: 'grp(a(admin@example.com)-b(projects/your-project-id/roles/customRole))',
color: 'subdued',
type: 'solid',
},
],
},
};
const baseGraph: EnhancedNodeViewModel[] = [ const baseGraph: EnhancedNodeViewModel[] = [
{ {
id: 'siem-windows', id: 'siem-windows',

View file

@ -6,24 +6,12 @@
*/ */
import React from 'react'; import React from 'react';
import type { import type { EuiIconProps, CommonProps, EuiListGroupItemProps } from '@elastic/eui';
EuiIconProps, import { useEuiTheme, EuiIcon, EuiListGroupItem, EuiText } from '@elastic/eui';
_EuiBackgroundColor,
CommonProps,
EuiListGroupItemProps,
} from '@elastic/eui';
import {
useEuiBackgroundColor,
useEuiTheme,
EuiIcon,
EuiListGroupItem,
EuiText,
} from '@elastic/eui';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
interface EuiColorProps { interface EuiColorProps {
color: keyof ReturnType<typeof useEuiTheme>['euiTheme']['colors']; color: keyof ReturnType<typeof useEuiTheme>['euiTheme']['colors'];
background: _EuiBackgroundColor;
} }
type IconContainerProps = EuiColorProps; type IconContainerProps = EuiColorProps;
@ -32,17 +20,10 @@ const IconContainer = styled.div<IconContainerProps>`
position: relative; position: relative;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%;
color: ${({ color }) => { color: ${({ color }) => {
const { euiTheme } = useEuiTheme(); const { euiTheme } = useEuiTheme();
return euiTheme.colors[color]; return euiTheme.colors[color];
}}; }};
background-color: ${({ background }) => useEuiBackgroundColor(background)};
border: 1px solid
${({ color }) => {
const { euiTheme } = useEuiTheme();
return euiTheme.colors[color];
}};
margin-right: 8px; margin-right: 8px;
`; `;
@ -53,10 +34,10 @@ const StyleEuiIcon = styled(EuiIcon)`
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
`; `;
type RoundedEuiIconProps = EuiIconProps & EuiColorProps; type PopoverListItemIconProps = EuiIconProps & EuiColorProps;
const RoundedEuiIcon = ({ color, background, ...rest }: RoundedEuiIconProps) => ( const PopoverListItemIcon = ({ color, ...rest }: PopoverListItemIconProps) => (
<IconContainer color={color} background={background}> <IconContainer color={color}>
<StyleEuiIcon color={color} {...rest} /> <StyleEuiIcon color={color} {...rest} />
</IconContainer> </IconContainer>
); );
@ -65,20 +46,11 @@ export const ExpandPopoverListItem = (
props: CommonProps & Pick<EuiListGroupItemProps, 'iconType' | 'label' | 'onClick'> props: CommonProps & Pick<EuiListGroupItemProps, 'iconType' | 'label' | 'onClick'>
) => { ) => {
const { iconType, label, onClick, ...rest } = props; const { iconType, label, onClick, ...rest } = props;
const { euiTheme } = useEuiTheme();
return ( return (
<EuiListGroupItem <EuiListGroupItem
{...rest} {...rest}
icon={ icon={iconType ? <PopoverListItemIcon color="text" type={iconType} size="m" /> : undefined}
iconType ? ( label={<EuiText>{label}</EuiText>}
<RoundedEuiIcon color="primary" background="primary" type={iconType} size="s" />
) : undefined
}
label={
<EuiText size="s" color={euiTheme.colors.primaryText}>
{label}
</EuiText>
}
onClick={onClick} onClick={onClick}
/> />
); );

View file

@ -12,7 +12,7 @@ import type {
LabelNodeDataModel, LabelNodeDataModel,
EdgeDataModel, EdgeDataModel,
NodeShape, NodeShape,
Color as NodeColor, NodeColor,
} from '@kbn/cloud-security-posture-common/types/graph/latest'; } from '@kbn/cloud-security-posture-common/types/graph/latest';
import type { Node, NodeProps as xyNodeProps, Edge, EdgeProps as xyEdgeProps } from '@xyflow/react'; import type { Node, NodeProps as xyNodeProps, Edge, EdgeProps as xyEdgeProps } from '@xyflow/react';

View file

@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
import type { Logger, IScopedClusterClient } from '@kbn/core/server'; import type { Logger, IScopedClusterClient } from '@kbn/core/server';
import { ApiMessageCode } from '@kbn/cloud-security-posture-common/types/graph/latest'; import { ApiMessageCode } from '@kbn/cloud-security-posture-common/types/graph/latest';
import type { import type {
Color, EdgeColor,
EdgeDataModel, EdgeDataModel,
EntityNodeDataModel, EntityNodeDataModel,
GraphRequest, GraphRequest,
@ -32,7 +32,6 @@ interface GraphEdge {
actorIds: string[] | string; actorIds: string[] | string;
action: string; action: string;
targetIds: string[] | string; targetIds: string[] | string;
eventOutcome: string;
isOrigin: boolean; isOrigin: boolean;
isOriginAlert: boolean; isOriginAlert: boolean;
} }
@ -154,7 +153,7 @@ const fetchGraph = async ({
esQuery?: EsQuery; esQuery?: EsQuery;
}): Promise<EsqlToRecords<GraphEdge>> => { }): Promise<EsqlToRecords<GraphEdge>> => {
const originAlertIds = originEventIds.filter((originEventId) => originEventId.isAlert); const originAlertIds = originEventIds.filter((originEventId) => originEventId.isAlert);
const query = `from logs-* const query = `FROM logs-*
| WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL | WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL
| EVAL isOrigin = ${ | EVAL isOrigin = ${
originEventIds.length > 0 originEventIds.length > 0
@ -170,10 +169,9 @@ const fetchGraph = async ({
ips = VALUES(related.ip), ips = VALUES(related.ip),
// hosts = VALUES(related.hosts), // hosts = VALUES(related.hosts),
users = VALUES(related.user) users = VALUES(related.user)
by actorIds = actor.entity.id, BY actorIds = actor.entity.id,
action = event.action, action = event.action,
targetIds = target.entity.id, targetIds = target.entity.id,
eventOutcome = event.outcome,
isOrigin, isOrigin,
isOriginAlert isOriginAlert
| LIMIT 1000 | LIMIT 1000
@ -260,17 +258,7 @@ const createNodes = (records: GraphEdge[], context: Omit<ParseContext, 'edgesMap
break; break;
} }
const { const { ips, hosts, users, actorIds, action, targetIds, isOriginAlert } = record;
ips,
hosts,
users,
actorIds,
action,
targetIds,
isOrigin,
isOriginAlert,
eventOutcome,
} = record;
const actorIdsArray = castArray(actorIds); const actorIdsArray = castArray(actorIds);
const targetIdsArray = castArray(targetIds); const targetIdsArray = castArray(targetIds);
const unknownTargets: string[] = []; const unknownTargets: string[] = [];
@ -289,7 +277,7 @@ const createNodes = (records: GraphEdge[], context: Omit<ParseContext, 'edgesMap
nodesMap[id] = { nodesMap[id] = {
id, id,
label: unknownTargets.includes(id) ? 'Unknown' : undefined, label: unknownTargets.includes(id) ? 'Unknown' : undefined,
color: isOriginAlert ? 'danger' : 'primary', color: 'primary',
...determineEntityNodeShape( ...determineEntityNodeShape(
id, id,
castArray(ips ?? []), castArray(ips ?? []),
@ -310,9 +298,9 @@ const createNodes = (records: GraphEdge[], context: Omit<ParseContext, 'edgesMap
} }
const labelNode: LabelNodeDataModel = { const labelNode: LabelNodeDataModel = {
id: edgeId + `label(${action})outcome(${eventOutcome})`, id: edgeId + `label(${action})`,
label: action, label: action,
color: isOriginAlert ? 'danger' : eventOutcome === 'failed' ? 'warning' : 'primary', color: isOriginAlert ? 'danger' : 'primary',
shape: 'label', shape: 'label',
}; };
@ -321,7 +309,7 @@ const createNodes = (records: GraphEdge[], context: Omit<ParseContext, 'edgesMap
labelEdges[labelNode.id] = { labelEdges[labelNode.id] = {
source: actorId, source: actorId,
target: targetId, target: targetId,
edgeType: isOrigin ? 'solid' : 'dashed', edgeType: 'solid',
}; };
} }
} }
@ -392,8 +380,19 @@ const createEdgesAndGroups = (context: ParseContext) => {
shape: 'group', shape: 'group',
}; };
nodesMap[groupNode.id] = groupNode; nodesMap[groupNode.id] = groupNode;
let groupEdgesColor: Color = 'primary'; let groupEdgesColor: EdgeColor = 'subdued';
let groupEdgesType: EdgeDataModel['type'] = 'dashed';
// Order of creation matters when using dagre layout, first create edges to the group node,
// then connect the group node to the label nodes
connectEntitiesAndLabelNode(
edgesMap,
nodesMap,
labelEdges[edgeLabelsIds[0]].source,
groupNode.id,
labelEdges[edgeLabelsIds[0]].target,
'solid',
groupEdgesColor
);
edgeLabelsIds.forEach((edgeLabelId) => { edgeLabelsIds.forEach((edgeLabelId) => {
(nodesMap[edgeLabelId] as Writable<LabelNodeDataModel>).parentId = groupNode.id; (nodesMap[edgeLabelId] as Writable<LabelNodeDataModel>).parentId = groupNode.id;
@ -408,28 +407,8 @@ const createEdgesAndGroups = (context: ParseContext) => {
if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') {
groupEdgesColor = 'danger'; groupEdgesColor = 'danger';
} else if (
(nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'warning' &&
groupEdgesColor !== 'danger'
) {
// Use warning only if there's no danger color
groupEdgesColor = 'warning';
}
if (labelEdges[edgeLabelId].edgeType === 'solid') {
groupEdgesType = 'solid';
} }
}); });
connectEntitiesAndLabelNode(
edgesMap,
nodesMap,
labelEdges[edgeLabelsIds[0]].source,
groupNode.id,
labelEdges[edgeLabelsIds[0]].target,
groupEdgesType,
groupEdgesColor
);
} }
}); });
}; };
@ -441,7 +420,7 @@ const connectEntitiesAndLabelNode = (
labelNodeId: string, labelNodeId: string,
targetNodeId: string, targetNodeId: string,
edgeType: EdgeDataModel['type'] = 'solid', edgeType: EdgeDataModel['type'] = 'solid',
colorOverride?: Color colorOverride?: EdgeColor
) => { ) => {
[ [
connectNodes(nodesMap, sourceNodeId, labelNodeId, edgeType, colorOverride), connectNodes(nodesMap, sourceNodeId, labelNodeId, edgeType, colorOverride),
@ -456,16 +435,15 @@ const connectNodes = (
sourceNodeId: string, sourceNodeId: string,
targetNodeId: string, targetNodeId: string,
edgeType: EdgeDataModel['type'] = 'solid', edgeType: EdgeDataModel['type'] = 'solid',
colorOverride?: Color colorOverride?: EdgeColor
): EdgeDataModel => { ): EdgeDataModel => {
const sourceNode = nodesMap[sourceNodeId]; const sourceNode = nodesMap[sourceNodeId];
const targetNode = nodesMap[targetNodeId]; const targetNode = nodesMap[targetNodeId];
const color = const color =
sourceNode.shape !== 'group' && targetNode.shape !== 'label' (sourceNode.shape === 'label' && sourceNode.color === 'danger') ||
? sourceNode.color (targetNode.shape === 'label' && targetNode.color === 'danger')
: targetNode.shape !== 'group' ? 'danger'
? targetNode.color : 'subdued';
: 'primary';
return { return {
id: `a(${sourceNodeId})-b(${targetNodeId})`, id: `a(${sourceNodeId})-b(${targetNodeId})`,

View file

@ -617,3 +617,128 @@
} }
} }
} }
{
"type": "doc",
"value": {
"data_stream": "logs-gcp.audit-default",
"id": "6",
"index": ".ds-logs-gcp.audit-default-2024.10.07-000001",
"source": {
"@timestamp": "2024-09-01T12:34:56.789Z",
"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.UpdateRole",
"agent_id_status": "missing",
"category": [
"session",
"network",
"configuration"
],
"id": "grouped2-event2",
"ingested": "2024-10-07T17:47:35Z",
"kind": "event",
"outcome": "success",
"provider": "activity",
"type": [
"end",
"access",
"allowed"
]
},
"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"
}
},
"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"
}
}
}
}

View file

@ -148,6 +148,13 @@ export default function (providerContext: FtrProviderContext) {
}, },
}, },
], ],
must_not: [
{
match_phrase: {
'event.action': 'google.iam.admin.v1.UpdateRole',
},
},
],
}, },
}, },
}, },
@ -168,10 +175,10 @@ export default function (providerContext: FtrProviderContext) {
response.body.edges.forEach((edge: any) => { response.body.edges.forEach((edge: any) => {
expect(edge).to.have.property('color'); expect(edge).to.have.property('color');
expect(edge.color).equal( expect(edge.color).equal(
'primary', 'subdued',
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
); );
expect(edge.type).equal('dashed'); expect(edge.type).equal('solid');
}); });
}); });
@ -191,7 +198,7 @@ export default function (providerContext: FtrProviderContext) {
response.body.nodes.forEach((node: any) => { response.body.nodes.forEach((node: any) => {
expect(node).to.have.property('color'); expect(node).to.have.property('color');
expect(node.color).equal( expect(node.color).equal(
'danger', node.shape === 'label' ? 'danger' : 'primary',
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
}); });
@ -230,7 +237,7 @@ export default function (providerContext: FtrProviderContext) {
response.body.edges.forEach((edge: any) => { response.body.edges.forEach((edge: any) => {
expect(edge).to.have.property('color'); expect(edge).to.have.property('color');
expect(edge.color).equal( expect(edge.color).equal(
'primary', 'subdued',
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
); );
expect(edge.type).equal('solid'); expect(edge.type).equal('solid');
@ -253,7 +260,7 @@ export default function (providerContext: FtrProviderContext) {
response.body.nodes.forEach((node: any) => { response.body.nodes.forEach((node: any) => {
expect(node).to.have.property('color'); expect(node).to.have.property('color');
expect(node.color).equal( expect(node.color).equal(
'danger', node.shape === 'label' ? 'danger' : 'primary',
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
}); });
@ -268,7 +275,7 @@ export default function (providerContext: FtrProviderContext) {
}); });
}); });
it('color of event of failed event should be warning', async () => { it('color of event of failed event should be subdued', async () => {
const response = await postGraph(supertest, { const response = await postGraph(supertest, {
query: { query: {
originEventIds: [], originEventIds: [],
@ -296,7 +303,7 @@ export default function (providerContext: FtrProviderContext) {
expect(node).to.have.property('color'); expect(node).to.have.property('color');
expect(node.color).equal( expect(node.color).equal(
node.shape === 'label' ? 'warning' : 'primary', 'primary',
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
}); });
@ -304,14 +311,14 @@ export default function (providerContext: FtrProviderContext) {
response.body.edges.forEach((edge: any) => { response.body.edges.forEach((edge: any) => {
expect(edge).to.have.property('color'); expect(edge).to.have.property('color');
expect(edge.color).equal( expect(edge.color).equal(
'warning', 'subdued',
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
); );
expect(edge.type).equal('dashed'); expect(edge.type).equal('solid');
}); });
}); });
it('2 grouped events, 1 failed, 1 success', async () => { it('2 grouped events', async () => {
const response = await postGraph(supertest, { const response = await postGraph(supertest, {
query: { query: {
originEventIds: [], originEventIds: [],
@ -322,7 +329,7 @@ export default function (providerContext: FtrProviderContext) {
filter: [ filter: [
{ {
match_phrase: { match_phrase: {
'actor.entity.id': 'admin3@example.com', 'actor.entity.id': 'admin@example.com',
}, },
}, },
], ],
@ -341,7 +348,7 @@ export default function (providerContext: FtrProviderContext) {
if (node.shape !== 'group') { if (node.shape !== 'group') {
expect(node).to.have.property('color'); expect(node).to.have.property('color');
expect(node.color).equal( expect(node.color).equal(
node.shape === 'label' && node.id.includes('outcome(failed)') ? 'warning' : 'primary', 'primary',
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
} }
@ -350,13 +357,10 @@ export default function (providerContext: FtrProviderContext) {
response.body.edges.forEach((edge: any) => { response.body.edges.forEach((edge: any) => {
expect(edge).to.have.property('color'); expect(edge).to.have.property('color');
expect(edge.color).equal( expect(edge.color).equal(
edge.id.includes('outcome(failed)') || 'subdued',
(edge.id.includes('grp(') && !edge.id.includes('outcome(success)'))
? 'warning'
: 'primary',
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
); );
expect(edge.type).equal('dashed'); expect(edge.type).equal('solid');
}); });
}); });
@ -379,7 +383,7 @@ export default function (providerContext: FtrProviderContext) {
response.body.nodes.forEach((node: any) => { response.body.nodes.forEach((node: any) => {
expect(node).to.have.property('color'); expect(node).to.have.property('color');
expect(node.color).equal( expect(node.color).equal(
'danger', node.shape === 'label' ? 'danger' : 'primary',
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
}); });
@ -421,10 +425,8 @@ export default function (providerContext: FtrProviderContext) {
response.body.nodes.forEach((node: any, idx: number) => { response.body.nodes.forEach((node: any, idx: number) => {
expect(node).to.have.property('color'); expect(node).to.have.property('color');
expect(node.color).equal( expect(node.color).equal(
idx <= 2 // First 3 nodes are expected to be colored as danger (ORDER MATTERS, alerts are expected to be first) idx === 2 // Only the label should be marked as danger (ORDER MATTERS, alerts are expected to be first)
? 'danger' ? 'danger'
: node.shape === 'label' && node.id.includes('outcome(failed)')
? 'warning'
: 'primary', : 'primary',
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
@ -433,10 +435,10 @@ export default function (providerContext: FtrProviderContext) {
response.body.edges.forEach((edge: any, idx: number) => { response.body.edges.forEach((edge: any, idx: number) => {
expect(edge).to.have.property('color'); expect(edge).to.have.property('color');
expect(edge.color).equal( expect(edge.color).equal(
idx <= 1 ? 'danger' : 'warning', idx <= 1 ? 'danger' : 'subdued',
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
); );
expect(edge.type).equal(idx <= 1 ? 'solid' : 'dashed'); expect(edge.type).equal('solid');
}); });
}); });

View file

@ -93,7 +93,7 @@ export class ExpandedFlyoutGraph extends GenericFtrService<SecurityTelemetryFtrP
const btnText = await this.testSubjects.getVisibleText( const btnText = await this.testSubjects.getVisibleText(
GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID
); );
expect(btnText).to.be('Hide actions on this entity'); expect(btnText).to.be('Hide actions done to this entity');
await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID); await this.testSubjects.click(GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_TEST_ID);
await this.pageObjects.header.waitUntilLoadingHasFinished(); await this.pageObjects.header.waitUntilLoadingHasFinished();
} }
@ -115,7 +115,7 @@ export class ExpandedFlyoutGraph extends GenericFtrService<SecurityTelemetryFtrP
const btnText = await this.testSubjects.getVisibleText( const btnText = await this.testSubjects.getVisibleText(
GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID
); );
expect(btnText).to.be('Hide events with this action'); expect(btnText).to.be('Hide related events');
await this.testSubjects.click(GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID); await this.testSubjects.click(GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID);
await this.pageObjects.header.waitUntilLoadingHasFinished(); await this.pageObjects.header.waitUntilLoadingHasFinished();
} }

View file

@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }: SecurityTelemetryFtrPro
// Show events with the same action // Show events with the same action
await expandedFlyoutGraph.showEventsOfSameAction( await expandedFlyoutGraph.showEventsOfSameAction(
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)' 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)'
); );
await expandedFlyoutGraph.expectFilterTextEquals( await expandedFlyoutGraph.expectFilterTextEquals(
0, 0,
@ -115,7 +115,7 @@ export default function ({ getPageObjects, getService }: SecurityTelemetryFtrPro
// Hide events with the same action // Hide events with the same action
await expandedFlyoutGraph.hideEventsOfSameAction( await expandedFlyoutGraph.hideEventsOfSameAction(
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)' 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)'
); );
await expandedFlyoutGraph.expectFilterTextEquals( await expandedFlyoutGraph.expectFilterTextEquals(
0, 0,

View file

@ -93,7 +93,7 @@ export default function ({ getPageObjects, getService }: SecurityTelemetryFtrPro
// Show events with the same action // Show events with the same action
await expandedFlyoutGraph.showEventsOfSameAction( await expandedFlyoutGraph.showEventsOfSameAction(
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)' 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)'
); );
await expandedFlyoutGraph.expectFilterTextEquals( await expandedFlyoutGraph.expectFilterTextEquals(
0, 0,
@ -106,7 +106,7 @@ export default function ({ getPageObjects, getService }: SecurityTelemetryFtrPro
// Hide events with the same action // Hide events with the same action
await expandedFlyoutGraph.hideEventsOfSameAction( await expandedFlyoutGraph.hideEventsOfSameAction(
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)' 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)'
); );
await expandedFlyoutGraph.expectFilterTextEquals( await expandedFlyoutGraph.expectFilterTextEquals(
0, 0,

View file

@ -72,7 +72,20 @@ export default function ({ getService }: FtrProviderContext) {
end: '2024-09-02T00:00:00Z', end: '2024-09-02T00:00:00Z',
esQuery: { esQuery: {
bool: { bool: {
filter: [{ match_phrase: { 'actor.entity.id': 'admin@example.com' } }], filter: [
{
match_phrase: {
'actor.entity.id': 'admin@example.com',
},
},
],
must_not: [
{
match_phrase: {
'event.action': 'google.iam.admin.v1.UpdateRole',
},
},
],
}, },
}, },
}, },
@ -89,6 +102,15 @@ export default function ({ getService }: FtrProviderContext) {
`node color mismatched [node: ${node.id}] [actual: ${node.color}]` `node color mismatched [node: ${node.id}] [actual: ${node.color}]`
); );
}); });
response.body.edges.forEach((edge: any) => {
expect(edge).to.have.property('color');
expect(edge.color).equal(
'subdued',
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
);
expect(edge.type).equal('solid');
});
}); });
}); });
}); });