mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cloud Security] Graph visualization and API (#195307)
## Summary This PR adds: - Graph visualization component using `xyflow`, and layouts the graph using `dagre`. - API that supports the graph visualization - API tests - Serverless API tests **List of open issues (will be tracked in a different ticket):** - Identify if `related.hosts`, `related.ip` and `related.user` are mapped before the query. (can be fixed by https://github.com/elastic/elasticsearch/issues/112912) - Update nodes rendering to match recent figma changes - Return 404 when feature is not enabled - Add keyboard accessibility - Resolve axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) ### How to test You can view the graph using storybook's [playground](https://supreme-adventure-8qjmlp1.pages.github.io/graph-storybook/?path=/story/components-graph-components-dagree-layout-graph--graph-stacked-edge-cases). To test this PR you can run ``` yarn storybook cloud_security_posture_packages ``` To test the API you can use the mocked data ```bash node scripts/es_archiver load x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit \ --es-url http://elastic:changeme@localhost:9200 \ --kibana-url http://elastic:changeme@localhost:5601 ``` And through dev tools: ``` POST kbn:/internal/cloud_security_posture/graph?apiVersion=1 { "query": { "actorIds": ["admin@example.com"], "eventIds": [""], "start": "now-1y/y", "end": "now/d" } } ``` ### Checklist Delete any items that are not applicable to this PR. - [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 - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2be7a203e1
commit
be0eadfb9f
66 changed files with 4611 additions and 27 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -92,6 +92,7 @@ x-pack/plugins/cloud_integrations/cloud_links @elastic/kibana-core
|
|||
x-pack/plugins/cloud @elastic/kibana-core
|
||||
x-pack/packages/kbn-cloud-security-posture @elastic/kibana-cloud-security-posture
|
||||
x-pack/packages/kbn-cloud-security-posture-common @elastic/kibana-cloud-security-posture
|
||||
x-pack/packages/kbn-cloud-security-posture/graph @elastic/kibana-cloud-security-posture
|
||||
x-pack/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture
|
||||
packages/shared-ux/code_editor/impl @elastic/appex-sharedux
|
||||
packages/shared-ux/code_editor/mocks @elastic/appex-sharedux
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
"@aws-crypto/util": "^5.2.0",
|
||||
"@babel/runtime": "^7.24.7",
|
||||
"@cfworker/json-schema": "^1.12.7",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
|
@ -219,6 +220,7 @@
|
|||
"@kbn/cloud-plugin": "link:x-pack/plugins/cloud",
|
||||
"@kbn/cloud-security-posture": "link:x-pack/packages/kbn-cloud-security-posture",
|
||||
"@kbn/cloud-security-posture-common": "link:x-pack/packages/kbn-cloud-security-posture-common",
|
||||
"@kbn/cloud-security-posture-graph": "link:x-pack/packages/kbn-cloud-security-posture/graph",
|
||||
"@kbn/cloud-security-posture-plugin": "link:x-pack/plugins/cloud_security_posture",
|
||||
"@kbn/code-editor": "link:packages/shared-ux/code_editor/impl",
|
||||
"@kbn/code-editor-mock": "link:packages/shared-ux/code_editor/mocks",
|
||||
|
@ -1054,6 +1056,7 @@
|
|||
"@turf/length": "^6.0.2",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"@xstate5/react": "npm:@xstate/react@^4.1.2",
|
||||
"@xyflow/react": "^12.3.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"ai": "^2.2.33",
|
||||
"ajv": "^8.12.0",
|
||||
|
@ -1309,6 +1312,7 @@
|
|||
"@babel/plugin-transform-class-properties": "^7.24.7",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.24.7",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.24.8",
|
||||
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
|
|
|
@ -543,6 +543,24 @@
|
|||
"labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"groupName": "@xyflow/react",
|
||||
"matchPackageNames": ["@xyflow/react"],
|
||||
"reviewers": ["team:kibana-cloud-security-posture"],
|
||||
"matchBaseBranches": ["main"],
|
||||
"labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"groupName": "@dagrejs/dagre",
|
||||
"matchPackageNames": ["@dagrejs/dagre"],
|
||||
"reviewers": ["team:kibana-cloud-security-posture"],
|
||||
"matchBaseBranches": ["main"],
|
||||
"labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
|
|
|
@ -16,6 +16,7 @@ export const storybookAliases = {
|
|||
canvas: 'x-pack/plugins/canvas/storybook',
|
||||
cases: 'packages/kbn-cases-components/.storybook',
|
||||
cell_actions: 'packages/kbn-cell-actions/.storybook',
|
||||
cloud_security_posture_packages: 'x-pack/packages/kbn-cloud-security-posture/storybook/config',
|
||||
cloud: 'packages/cloud/.storybook',
|
||||
coloring: 'packages/kbn-coloring/.storybook',
|
||||
language_documentation_popover: 'packages/kbn-language-documentation/.storybook',
|
||||
|
|
|
@ -178,6 +178,8 @@
|
|||
"@kbn/cloud-security-posture/*": ["x-pack/packages/kbn-cloud-security-posture/*"],
|
||||
"@kbn/cloud-security-posture-common": ["x-pack/packages/kbn-cloud-security-posture-common"],
|
||||
"@kbn/cloud-security-posture-common/*": ["x-pack/packages/kbn-cloud-security-posture-common/*"],
|
||||
"@kbn/cloud-security-posture-graph": ["x-pack/packages/kbn-cloud-security-posture/graph"],
|
||||
"@kbn/cloud-security-posture-graph/*": ["x-pack/packages/kbn-cloud-security-posture/graph/*"],
|
||||
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
|
||||
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
|
||||
"@kbn/code-editor": ["packages/shared-ux/code_editor/impl"],
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * as graphV1 from './v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
export const graphRequestSchema = schema.object({
|
||||
query: schema.object({
|
||||
actorIds: schema.arrayOf(schema.string()),
|
||||
eventIds: schema.arrayOf(schema.string()),
|
||||
// TODO: use zod for range validation instead of config schema
|
||||
start: schema.oneOf([schema.number(), schema.string()]),
|
||||
end: schema.oneOf([schema.number(), schema.string()]),
|
||||
}),
|
||||
});
|
||||
|
||||
export const graphResponseSchema = () =>
|
||||
schema.object({
|
||||
nodes: schema.arrayOf(
|
||||
schema.oneOf([entityNodeDataSchema, groupNodeDataSchema, labelNodeDataSchema])
|
||||
),
|
||||
edges: schema.arrayOf(edgeDataSchema),
|
||||
});
|
||||
|
||||
export const colorSchema = schema.oneOf([
|
||||
schema.literal('primary'),
|
||||
schema.literal('danger'),
|
||||
schema.literal('warning'),
|
||||
]);
|
||||
|
||||
export const nodeShapeSchema = schema.oneOf([
|
||||
schema.literal('hexagon'),
|
||||
schema.literal('pentagon'),
|
||||
schema.literal('ellipse'),
|
||||
schema.literal('rectangle'),
|
||||
schema.literal('diamond'),
|
||||
schema.literal('label'),
|
||||
schema.literal('group'),
|
||||
]);
|
||||
|
||||
export const nodeBaseDataSchema = schema.object({
|
||||
id: schema.string(),
|
||||
label: schema.maybe(schema.string()),
|
||||
icon: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export const entityNodeDataSchema = schema.allOf([
|
||||
nodeBaseDataSchema,
|
||||
schema.object({
|
||||
color: colorSchema,
|
||||
shape: schema.oneOf([
|
||||
schema.literal('hexagon'),
|
||||
schema.literal('pentagon'),
|
||||
schema.literal('ellipse'),
|
||||
schema.literal('rectangle'),
|
||||
schema.literal('diamond'),
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const groupNodeDataSchema = schema.allOf([
|
||||
nodeBaseDataSchema,
|
||||
schema.object({
|
||||
shape: schema.literal('group'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const labelNodeDataSchema = schema.allOf([
|
||||
nodeBaseDataSchema,
|
||||
schema.object({
|
||||
source: schema.string(),
|
||||
target: schema.string(),
|
||||
shape: schema.literal('label'),
|
||||
parentId: schema.maybe(schema.string()),
|
||||
color: colorSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const edgeDataSchema = schema.object({
|
||||
id: schema.string(),
|
||||
source: schema.string(),
|
||||
sourceShape: nodeShapeSchema,
|
||||
target: schema.string(),
|
||||
targetShape: nodeShapeSchema,
|
||||
color: colorSchema,
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * as graphV1 from './v1';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 type { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
colorSchema,
|
||||
edgeDataSchema,
|
||||
entityNodeDataSchema,
|
||||
graphRequestSchema,
|
||||
graphResponseSchema,
|
||||
groupNodeDataSchema,
|
||||
labelNodeDataSchema,
|
||||
nodeShapeSchema,
|
||||
} from '../../schema/graph/v1';
|
||||
|
||||
export type GraphRequest = TypeOf<typeof graphRequestSchema>;
|
||||
export type GraphResponse = TypeOf<typeof graphResponseSchema>;
|
||||
|
||||
export type Color = typeof colorSchema.type;
|
||||
|
||||
export type NodeShape = TypeOf<typeof nodeShapeSchema>;
|
||||
|
||||
export type EntityNodeDataModel = TypeOf<typeof entityNodeDataSchema>;
|
||||
|
||||
export type GroupNodeDataModel = TypeOf<typeof groupNodeDataSchema>;
|
||||
|
||||
export type LabelNodeDataModel = TypeOf<typeof labelNodeDataSchema>;
|
||||
|
||||
export type EdgeDataModel = TypeOf<typeof edgeDataSchema>;
|
||||
|
||||
export type NodeDataModel = EntityNodeDataModel | GroupNodeDataModel | LabelNodeDataModel;
|
|
@ -4,6 +4,13 @@ This package includes
|
|||
- Hooks that's used on Flyout component that's used in Alerts page on Security Solution Plugins as well as components on CSP plugin
|
||||
- Utilities and types thats used for the Hooks above as well as in CSP plugins
|
||||
|
||||
## Storybook
|
||||
|
||||
General look of the component can be checked visually running the following storybook:
|
||||
`yarn storybook cloud_security_posture_graph`
|
||||
|
||||
Note that all the interactions are mocked.
|
||||
|
||||
## Maintainers
|
||||
|
||||
Maintained by the Cloud Security Team
|
25
x-pack/packages/kbn-cloud-security-posture/graph/README.md
Normal file
25
x-pack/packages/kbn-cloud-security-posture/graph/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Cloud Security Posture's Graph
|
||||
|
||||
## Motivation
|
||||
|
||||
The idea behind this package is to have a reusable graph component, embedding the features available to alerts flyout in
|
||||
security solution plugin.
|
||||
|
||||
## How to use this
|
||||
|
||||
Standalone examples will follow. In the meantime checkout storybook to view the graphs progress.
|
||||
|
||||
## The most important public api members
|
||||
|
||||
- GraphComponent itself (comming soon..)
|
||||
|
||||
### Extras
|
||||
|
||||
Be sure to check out provided helpers
|
||||
|
||||
## Storybook
|
||||
|
||||
General look of the component can be checked visually running the following storybook:
|
||||
`yarn storybook cloud_security_posture_packages`
|
||||
|
||||
Note that all the interactions are mocked.
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
roots: ['<rootDir>/x-pack/packages/kbn-cloud-security-posture/graph'],
|
||||
rootDir: '../../../..',
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/cloud-security-posture-graph",
|
||||
"owner": "@elastic/kibana-cloud-security-posture"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/cloud-security-posture-graph",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.76413 10.4088C6.76413 10.7007 6.79606 10.9373 6.85193 11.1108C6.91578 11.2844 6.9956 11.4737 7.10734 11.6788C7.14725 11.7419 7.16321 11.805 7.16321 11.8602C7.16321 11.9391 7.11532 12.018 7.01156 12.0968L6.50873 12.4281C6.43689 12.4755 6.36506 12.4991 6.30121 12.4991C6.22139 12.4991 6.14158 12.4597 6.06176 12.3887C5.95002 12.2704 5.85424 12.1442 5.77443 12.018C5.69461 11.8839 5.6148 11.734 5.527 11.5526C4.90445 12.2783 4.12226 12.6411 3.18044 12.6411C2.51 12.6411 1.97524 12.4518 1.58414 12.0732C1.19305 11.6946 0.993512 11.1897 0.993512 10.5587C0.993512 9.88818 1.23296 9.3439 1.71983 8.93372C2.2067 8.52354 2.8532 8.31845 3.6753 8.31845C3.94667 8.31845 4.22602 8.34212 4.52133 8.38156C4.81665 8.421 5.11995 8.4841 5.43921 8.55509V7.97926C5.43921 7.37977 5.3115 6.9617 5.06408 6.71717C4.80867 6.47264 4.37767 6.35432 3.76309 6.35432C3.48374 6.35432 3.19641 6.38587 2.90109 6.45686C2.60577 6.52786 2.31844 6.61462 2.03909 6.72506C1.91138 6.78027 1.81561 6.81183 1.75974 6.8276C1.70387 6.84338 1.66396 6.85127 1.63203 6.85127C1.52029 6.85127 1.46442 6.77239 1.46442 6.60674V6.22022C1.46442 6.09401 1.48038 5.99935 1.52029 5.94414C1.5602 5.88892 1.63203 5.8337 1.74377 5.77849C2.02313 5.6365 2.35835 5.51818 2.74944 5.42352C3.14054 5.32098 3.55557 5.27365 3.99456 5.27365C4.94435 5.27365 5.63874 5.48663 6.08571 5.91258C6.52469 6.33854 6.74817 6.98536 6.74817 7.85305V10.4088H6.76413ZM3.52365 11.6078C3.78704 11.6078 4.05841 11.5605 4.34574 11.4658C4.63308 11.3711 4.88848 11.1976 5.10398 10.961C5.23169 10.8111 5.32747 10.6454 5.37535 10.4561C5.42324 10.2668 5.45517 10.0381 5.45517 9.76986V9.43856C5.22371 9.38334 4.97628 9.33601 4.72087 9.30446C4.46546 9.27291 4.21804 9.25713 3.97061 9.25713C3.43585 9.25713 3.04476 9.35968 2.78137 9.57266C2.51798 9.78563 2.39027 10.0854 2.39027 10.4798C2.39027 10.8505 2.48605 11.1266 2.68559 11.3159C2.87715 11.5131 3.1565 11.6078 3.52365 11.6078ZM9.93279 12.4597C9.78912 12.4597 9.69334 12.436 9.62949 12.3808C9.56564 12.3335 9.50977 12.2231 9.46188 12.0732L7.58623 5.97569C7.53834 5.81793 7.5144 5.71538 7.5144 5.66017C7.5144 5.53396 7.57825 5.46296 7.70595 5.46296H8.48814C8.63979 5.46296 8.74355 5.48663 8.79942 5.54184C8.86327 5.58917 8.91116 5.69961 8.95905 5.84948L10.2999 11.0714L11.5451 5.84948C11.585 5.69172 11.6328 5.58917 11.6967 5.54184C11.7606 5.49452 11.8723 5.46296 12.016 5.46296H12.6545C12.8061 5.46296 12.9099 5.48663 12.9737 5.54184C13.0376 5.58917 13.0935 5.69961 13.1254 5.84948L14.3865 11.1345L15.7673 5.84948C15.8152 5.69172 15.871 5.58917 15.9269 5.54184C15.9907 5.49452 16.0945 5.46296 16.2382 5.46296H16.9805C17.1082 5.46296 17.18 5.52607 17.18 5.66017C17.18 5.69961 17.172 5.73905 17.164 5.78638C17.156 5.8337 17.1401 5.89681 17.1082 5.98358L15.1846 12.0811C15.1367 12.2388 15.0809 12.3414 15.017 12.3887C14.9531 12.436 14.8494 12.4676 14.7137 12.4676H14.0273C13.8756 12.4676 13.7719 12.4439 13.708 12.3887C13.6442 12.3335 13.5883 12.2309 13.5564 12.0732L12.3193 6.98536L11.0901 12.0653C11.0502 12.2231 11.0023 12.3256 10.9385 12.3808C10.8746 12.436 10.7629 12.4597 10.6192 12.4597H9.93279ZM20.189 12.6727C19.774 12.6727 19.3589 12.6253 18.9599 12.5307C18.5608 12.436 18.2495 12.3335 18.042 12.2152C17.9143 12.1442 17.8265 12.0653 17.7946 11.9943C17.7626 11.9233 17.7467 11.8444 17.7467 11.7734V11.3711C17.7467 11.2055 17.8105 11.1266 17.9303 11.1266C17.9781 11.1266 18.026 11.1345 18.0739 11.1503C18.1218 11.166 18.1936 11.1976 18.2735 11.2292C18.5448 11.3475 18.8401 11.4421 19.1514 11.5052C19.4707 11.5683 19.782 11.5999 20.1012 11.5999C20.6041 11.5999 20.9951 11.5131 21.2665 11.3396C21.5379 11.1661 21.6816 10.9136 21.6816 10.5902C21.6816 10.3694 21.6097 10.1879 21.4661 10.0381C21.3224 9.88818 21.051 9.75408 20.6599 9.62787L19.5026 9.27291C18.92 9.09148 18.489 8.82329 18.2256 8.46832C17.9622 8.12125 17.8265 7.73473 17.8265 7.32455C17.8265 6.99325 17.8983 6.70139 18.042 6.44897C18.1857 6.19656 18.3772 5.97569 18.6167 5.80215C18.8561 5.62073 19.1275 5.48663 19.4467 5.39197C19.766 5.29731 20.1012 5.25787 20.4524 5.25787C20.628 5.25787 20.8116 5.26576 20.9872 5.28943C21.1707 5.31309 21.3383 5.34464 21.506 5.37619C21.6656 5.41564 21.8172 5.45508 21.9609 5.5024C22.1046 5.54973 22.2163 5.59706 22.2961 5.64439C22.4079 5.70749 22.4877 5.7706 22.5356 5.84159C22.5835 5.9047 22.6074 5.99147 22.6074 6.1019V6.47264C22.6074 6.63829 22.5436 6.72506 22.4238 6.72506C22.36 6.72506 22.2562 6.69351 22.1205 6.6304C21.6656 6.42531 21.1548 6.32277 20.5881 6.32277C20.1331 6.32277 19.774 6.39376 19.5265 6.54363C19.2791 6.69351 19.1514 6.92226 19.1514 7.24567C19.1514 7.46654 19.2312 7.65585 19.3909 7.80573C19.5505 7.9556 19.8458 8.10547 20.2688 8.23957L21.4022 8.59453C21.9769 8.77596 22.3919 9.02838 22.6393 9.35179C22.8868 9.6752 23.0065 10.0459 23.0065 10.4561C23.0065 10.7953 22.9346 11.1029 22.799 11.3711C22.6553 11.6393 22.4637 11.876 22.2163 12.0653C21.9689 12.2625 21.6736 12.4045 21.3304 12.507C20.9712 12.6175 20.5961 12.6727 20.189 12.6727Z" fill="#252F3E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.6975 16.5066C19.0716 18.4234 15.2564 19.441 11.9761 19.441C7.37871 19.441 3.23631 17.7608 0.107566 14.9685C-0.13986 14.7476 0.0836217 14.4478 0.378937 14.6214C3.76309 16.5618 7.93741 17.7372 12.2554 17.7372C15.1686 17.7372 18.3692 17.1377 21.3144 15.9071C21.7534 15.7099 22.1285 16.1911 21.6975 16.5066Z" fill="#FF9900"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.791 15.2761C22.4558 14.8501 20.5721 15.071 19.7181 15.1735C19.4627 15.2051 19.4228 14.9842 19.6543 14.8186C21.1548 13.7774 23.6211 14.0771 23.9084 14.4242C24.1957 14.7791 23.8286 17.2166 22.4238 18.384C22.2083 18.5654 22.0008 18.4708 22.0966 18.2341C22.4159 17.4532 23.1262 15.6942 22.791 15.2761Z" fill="#FF9900"/>
|
||||
</svg>
|
After Width: | Height: | Size: 5.7 KiB |
|
@ -0,0 +1,15 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2302_45405)">
|
||||
<path d="M24 0H0V24H24V0Z" fill="url(#paint0_linear_2302_45405)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.1 15.9H15.6V8.4H8.1V15.9ZM16.2 8.4H17.4V9H16.2V10.2H17.4V10.8H16.2V11.7H17.4V12.3H16.2V13.5H17.4V14.1H16.2V15.3H17.4V15.9H16.2V15.9408C16.2 16.2492 15.9492 16.5 15.6408 16.5H15.6V17.7H15V16.5H13.8V17.7H13.2V16.5H12.3V17.7H11.7V16.5H10.5V17.7H9.9V16.5H8.7V17.7H8.1V16.5H8.0592C7.7508 16.5 7.5 16.2492 7.5 15.9408V15.9H6.6V15.3H7.5V14.1H6.6V13.5H7.5V12.3H6.6V11.7H7.5V10.8H6.6V10.2H7.5V9H6.6V8.4H7.5V8.3592C7.5 8.0508 7.7508 7.8 8.0592 7.8H8.1V6.6H8.7V7.8H9.9V6.6H10.5V7.8H11.7V6.6H12.3V7.8H13.2V6.6H13.8V7.8H15V6.6H15.6V7.8H15.6408C15.9492 7.8 16.2 8.0508 16.2 8.3592V8.4ZM12.3 19.7628C12.3 19.7832 12.2832 19.8 12.2628 19.8H4.2372C4.2168 19.8 4.2 19.7832 4.2 19.7628V11.7372C4.2 11.7168 4.2168 11.7 4.2372 11.7H6V11.1H4.2372C3.8859 11.1 3.6 11.3859 3.6 11.7372V19.7628C3.6 20.1141 3.8859 20.4 4.2372 20.4H12.2628C12.6141 20.4 12.9 20.1141 12.9 19.7628V18.3H12.3V19.7628ZM20.4 4.2372V12.2628C20.4 12.6141 20.1141 12.9 19.7628 12.9H18V12.3H19.7628C19.7832 12.3 19.8 12.2832 19.8 12.2628V4.2372C19.8 4.2168 19.7832 4.2 19.7628 4.2H11.7372C11.7168 4.2 11.7 4.2168 11.7 4.2372V6H11.1V4.2372C11.1 3.8859 11.3859 3.6 11.7372 3.6H19.7628C20.1141 3.6 20.4 3.8859 20.4 4.2372Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2302_45405" x1="0" y1="2400" x2="2400" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C8511B"/>
|
||||
<stop offset="1" stop-color="#FF9900"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2302_45405">
|
||||
<rect width="24" height="24" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,14 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.1777 17.7847L20.5053 19.7997V4.164L12.1777 6.18033V17.7847Z" fill="#8C3123"/>
|
||||
<path d="M20.5053 4.164L22.106 4.964V19.0043L20.5057 19.804L20.5053 4.164Z" fill="#E05243"/>
|
||||
<path d="M15.81 6.952L12.1777 6.05333V0L15.81 1.816V6.952Z" fill="#E05243"/>
|
||||
<path d="M12.1777 24L15.8097 22.1847V17.0487L12.1777 17.947V24Z" fill="#E05243"/>
|
||||
<path d="M15.81 14.5713L12.1777 15.0337V8.98L15.81 9.43567V14.5713Z" fill="#E05243"/>
|
||||
<path d="M12.1777 17.7847L3.84967 19.7997V4.164L12.1777 6.18033V17.7847Z" fill="#E05243"/>
|
||||
<path d="M3.84967 4.164L2.25 4.964V19.0043L3.84967 19.804L3.84967 4.164Z" fill="#8C3123"/>
|
||||
<path d="M8.54633 6.952L12.1777 6.05333V0L8.54633 1.816V6.952Z" fill="#8C3123"/>
|
||||
<path d="M12.1777 24L8.546 22.1847V17.0487L12.1777 17.947V24Z" fill="#8C3123"/>
|
||||
<path d="M8.54633 14.5713L12.1777 15.0337V8.98L8.54633 9.43567V14.5713Z" fill="#8C3123"/>
|
||||
<path d="M15.81 6.952L12.1777 7.614L8.54633 6.952L12.1777 6.05333L15.81 6.952Z" fill="#5E1F18"/>
|
||||
<path d="M15.8097 17.0487L12.1777 16.382L8.546 17.0487L12.1777 17.9527L15.8097 17.0487Z" fill="#F2B0A9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.1 KiB |
|
@ -0,0 +1,607 @@
|
|||
/*
|
||||
* 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 { ThemeProvider } from '@emotion/react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Controls,
|
||||
Background,
|
||||
Node,
|
||||
Edge,
|
||||
Position,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from '@xyflow/react';
|
||||
import { Story } from '@storybook/react';
|
||||
import type {
|
||||
EdgeDataModel,
|
||||
LabelNodeDataModel,
|
||||
NodeDataModel,
|
||||
} from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
import { Writable } from '@kbn/utility-types';
|
||||
import {
|
||||
HexagonNode,
|
||||
PentagonNode,
|
||||
EllipseNode,
|
||||
RectangleNode,
|
||||
DiamondNode,
|
||||
LabelNode,
|
||||
EdgeGroupNode,
|
||||
} from './node';
|
||||
import type { NodeViewModel } from './types';
|
||||
import { DefaultEdge } from './edge';
|
||||
import { SvgDefsMarker } from './edge/styles';
|
||||
import { GroupStyleOverride } from './node/styles';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { layoutGraph } from './graph/layout_graph';
|
||||
|
||||
export default {
|
||||
title: 'Components/Graph Components/Dagree Layout Graph',
|
||||
description: 'CDR - Graph visualization',
|
||||
};
|
||||
|
||||
const nodeTypes = {
|
||||
hexagon: HexagonNode,
|
||||
pentagon: PentagonNode,
|
||||
ellipse: EllipseNode,
|
||||
rectangle: RectangleNode,
|
||||
diamond: DiamondNode,
|
||||
label: LabelNode,
|
||||
group: EdgeGroupNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
default: DefaultEdge,
|
||||
};
|
||||
|
||||
interface GraphData {
|
||||
nodes: NodeDataModel[];
|
||||
edges: EdgeDataModel[];
|
||||
interactive: boolean;
|
||||
}
|
||||
|
||||
const extractEdges = (
|
||||
graphData: NodeDataModel[]
|
||||
): { nodes: NodeDataModel[]; edges: EdgeDataModel[] } => {
|
||||
// Process nodes, transform nodes of id in the format of a(source)-b(target) to edges from a to label and from label to b
|
||||
// If there are multiple edges from a to b, create a parent node and group the labels under it. The parent node will be a group node.
|
||||
// Connect from a to the group node and from the group node to all the labels. and from the labels to the group again and from the group to b.
|
||||
const nodesMetadata: { [key: string]: { edgesIn: number; edgesOut: number } } = {};
|
||||
const edgesMetadata: {
|
||||
[key: string]: { source: string; target: string; edgesStacked: number; edges: string[] };
|
||||
} = {};
|
||||
const labelsMetadata: {
|
||||
[key: string]: { source: string; target: string; labelsNodes: LabelNodeDataModel[] };
|
||||
} = {};
|
||||
const nodes: { [key: string]: NodeDataModel } = {};
|
||||
const edges: EdgeDataModel[] = [];
|
||||
|
||||
graphData.forEach((node) => {
|
||||
if (node.shape === 'label') {
|
||||
const labelNode = { ...node, id: `${node.id}label(${node.label})` };
|
||||
const { source, target } = node;
|
||||
|
||||
if (labelsMetadata[node.id]) {
|
||||
labelsMetadata[node.id].labelsNodes.push(labelNode);
|
||||
} else {
|
||||
labelsMetadata[node.id] = { source, target, labelsNodes: [labelNode] };
|
||||
}
|
||||
|
||||
nodes[labelNode.id] = labelNode;
|
||||
|
||||
// Set metadata
|
||||
const edgeId = node.id;
|
||||
nodesMetadata[source].edgesOut += 1; // TODO: Check if source exists
|
||||
nodesMetadata[target].edgesIn += 1; // TODO: Check if target exists
|
||||
|
||||
if (edgesMetadata[edgeId]) {
|
||||
edgesMetadata[edgeId].edgesStacked += 1;
|
||||
edgesMetadata[edgeId].edges.push(edgeId);
|
||||
} else {
|
||||
edgesMetadata[edgeId] = {
|
||||
source,
|
||||
target,
|
||||
edgesStacked: 1,
|
||||
edges: [labelNode.id],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
nodes[node.id] = node;
|
||||
nodesMetadata[node.id] = { edgesIn: 0, edgesOut: 0 };
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(labelsMetadata).forEach((edge) => {
|
||||
if (edge.labelsNodes.length > 1) {
|
||||
const groupNode: NodeDataModel = {
|
||||
id: `grp(a(${edge.source})-b(${edge.target}))`,
|
||||
shape: 'group',
|
||||
};
|
||||
|
||||
nodes[groupNode.id] = groupNode;
|
||||
edges.push({
|
||||
id: `a(${edge.source})-b(${groupNode.id})`,
|
||||
source: edge.source,
|
||||
sourceShape: nodes[edge.source].shape,
|
||||
target: groupNode.id,
|
||||
targetShape: groupNode.shape,
|
||||
color: edge.labelsNodes[0].color,
|
||||
});
|
||||
|
||||
edges.push({
|
||||
id: `a(${groupNode.id})-b(${edge.target})`,
|
||||
source: groupNode.id,
|
||||
sourceShape: groupNode.shape,
|
||||
target: edge.target,
|
||||
targetShape: nodes[edge.target].shape,
|
||||
color: edge.labelsNodes[0].color,
|
||||
});
|
||||
|
||||
edge.labelsNodes.forEach((labelNode: Writable<LabelNodeDataModel>) => {
|
||||
labelNode.parentId = groupNode.id;
|
||||
|
||||
edges.push({
|
||||
id: `a(${groupNode.id})-b(${labelNode.id})`,
|
||||
source: groupNode.id,
|
||||
sourceShape: groupNode.shape,
|
||||
target: labelNode.id,
|
||||
targetShape: labelNode.shape,
|
||||
color: labelNode.color,
|
||||
});
|
||||
|
||||
edges.push({
|
||||
id: `a(${labelNode.id})-b(${groupNode.id})`,
|
||||
source: labelNode.id,
|
||||
sourceShape: labelNode.shape,
|
||||
target: groupNode.id,
|
||||
targetShape: groupNode.shape,
|
||||
color: labelNode.color,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
edges.push({
|
||||
id: `a(${edge.source})-b(${edge.labelsNodes[0].id})`,
|
||||
source: edge.source,
|
||||
sourceShape: nodes[edge.source].shape,
|
||||
target: edge.labelsNodes[0].id,
|
||||
targetShape: edge.labelsNodes[0].shape,
|
||||
color: edge.labelsNodes[0].color,
|
||||
});
|
||||
|
||||
edges.push({
|
||||
id: `a(${edge.labelsNodes[0].id})-b(${edge.target})`,
|
||||
source: edge.labelsNodes[0].id,
|
||||
sourceShape: edge.labelsNodes[0].shape,
|
||||
target: edge.target,
|
||||
targetShape: nodes[edge.target].shape,
|
||||
color: edge.labelsNodes[0].color,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reversing order, groups like to be first in order :D
|
||||
return { nodes: Object.values(nodes).reverse(), edges };
|
||||
};
|
||||
|
||||
const Template: Story<GraphData> = ({ nodes, edges }: GraphData) => {
|
||||
const { initialNodes, initialEdges } = processGraph(nodes, edges);
|
||||
|
||||
const [nodesState, _setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edgesState, _setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={{ darkMode: false }}>
|
||||
<SvgDefsMarker />
|
||||
<ReactFlow
|
||||
fitView
|
||||
attributionPosition={undefined}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodes={nodesState}
|
||||
edges={edgesState}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const SimpleAPIMock = Template.bind({});
|
||||
SimpleAPIMock.args = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'admin@example.com',
|
||||
label: 'admin@example.com',
|
||||
color: 'primary',
|
||||
shape: 'ellipse',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
id: 'projects/your-project-id/roles/customRole',
|
||||
label: 'projects/your-project-id/roles/customRole',
|
||||
color: 'primary',
|
||||
shape: 'hexagon',
|
||||
icon: 'questionInCircle',
|
||||
},
|
||||
{
|
||||
id: 'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)',
|
||||
label: 'google.iam.admin.v1.CreateRole',
|
||||
source: 'admin@example.com',
|
||||
target: 'projects/your-project-id/roles/customRole',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'a(admin@example.com)-b(a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole))',
|
||||
source: 'admin@example.com',
|
||||
sourceShape: 'ellipse',
|
||||
target:
|
||||
'a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)',
|
||||
targetShape: 'label',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
id: 'a(a(admin@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole))-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)',
|
||||
sourceShape: 'label',
|
||||
target: 'projects/your-project-id/roles/customRole',
|
||||
targetShape: 'hexagon',
|
||||
color: 'primary',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const GroupWithWarningAPIMock = Template.bind({});
|
||||
GroupWithWarningAPIMock.args = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
shape: 'group',
|
||||
},
|
||||
{
|
||||
id: 'admin3@example.com',
|
||||
label: 'admin3@example.com',
|
||||
color: 'primary',
|
||||
shape: 'ellipse',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
id: 'projects/your-project-id/roles/customRole',
|
||||
label: 'projects/your-project-id/roles/customRole',
|
||||
color: 'primary',
|
||||
shape: 'hexagon',
|
||||
icon: 'questionInCircle',
|
||||
},
|
||||
{
|
||||
id: 'a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(failed)',
|
||||
label: 'google.iam.admin.v1.CreateRole',
|
||||
source: 'admin3@example.com',
|
||||
target: 'projects/your-project-id/roles/customRole',
|
||||
color: 'warning',
|
||||
shape: 'label',
|
||||
parentId: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
},
|
||||
{
|
||||
id: 'a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)',
|
||||
label: 'google.iam.admin.v1.CreateRole',
|
||||
source: 'admin3@example.com',
|
||||
target: 'projects/your-project-id/roles/customRole',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
parentId: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'a(admin3@example.com)-b(grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)))',
|
||||
source: 'admin3@example.com',
|
||||
sourceShape: 'ellipse',
|
||||
target: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
targetShape: 'group',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
id: 'a(grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)))-b(projects/your-project-id/roles/customRole)',
|
||||
source: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
sourceShape: 'group',
|
||||
target: 'projects/your-project-id/roles/customRole',
|
||||
targetShape: 'hexagon',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
id: 'a(grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)))-b(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(failed))',
|
||||
source: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
sourceShape: 'group',
|
||||
target:
|
||||
'a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(failed)',
|
||||
targetShape: 'label',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
id: 'a(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(failed))-b(grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)))',
|
||||
source:
|
||||
'a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(failed)',
|
||||
sourceShape: 'label',
|
||||
target: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
targetShape: 'group',
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
id: 'a(grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)))-b(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success))',
|
||||
source: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
sourceShape: 'group',
|
||||
target:
|
||||
'a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)',
|
||||
targetShape: 'label',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
id: 'a(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success))-b(grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole)))',
|
||||
source:
|
||||
'a(admin3@example.com)-b(projects/your-project-id/roles/customRole)label(google.iam.admin.v1.CreateRole)outcome(success)',
|
||||
sourceShape: 'label',
|
||||
target: 'grp(a(admin3@example.com)-b(projects/your-project-id/roles/customRole))',
|
||||
targetShape: 'group',
|
||||
color: 'primary',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const Graph = Template.bind({});
|
||||
const baseGraph: NodeDataModel[] = [
|
||||
{
|
||||
id: 'siem-windows',
|
||||
label: '',
|
||||
color: 'danger',
|
||||
shape: 'hexagon',
|
||||
icon: 'storage',
|
||||
},
|
||||
{
|
||||
id: '213.180.204.3',
|
||||
label: 'IP: 213.180.204.3',
|
||||
color: 'danger',
|
||||
shape: 'diamond',
|
||||
icon: 'globe',
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
label: '',
|
||||
color: 'danger',
|
||||
shape: 'ellipse',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
id: 'oktauser',
|
||||
label: 'pluni@elastic.co',
|
||||
color: 'primary',
|
||||
shape: 'ellipse',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
id: 'hackeruser',
|
||||
label: 'Hacker',
|
||||
color: 'primary',
|
||||
shape: 'ellipse',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
label: 'Customer PII Data',
|
||||
color: 'primary',
|
||||
shape: 'rectangle',
|
||||
icon: 'aws_s3',
|
||||
},
|
||||
{
|
||||
id: 'ec2',
|
||||
label: 'AWS::EC2',
|
||||
color: 'primary',
|
||||
shape: 'rectangle',
|
||||
icon: 'aws_ec2',
|
||||
},
|
||||
{
|
||||
id: 'aws',
|
||||
label: 'AWS CloudTrail',
|
||||
color: 'primary',
|
||||
shape: 'rectangle',
|
||||
icon: 'aws',
|
||||
},
|
||||
{
|
||||
id: 'a(siem-windows)-b(user)',
|
||||
source: 'siem-windows',
|
||||
target: 'user',
|
||||
label: 'User login to OKTA',
|
||||
color: 'danger',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(213.180.204.3)-b(user)',
|
||||
source: '213.180.204.3',
|
||||
target: 'user',
|
||||
label: 'User login to OKTA',
|
||||
color: 'danger',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(user)-b(oktauser)',
|
||||
source: 'user',
|
||||
target: 'oktauser',
|
||||
label: 'user.authentication.sso',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(user)-b(oktauser)',
|
||||
source: 'user',
|
||||
target: 'oktauser',
|
||||
label: 'AssumeRoleWithSAML',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(oktauser)-b(hackeruser)',
|
||||
source: 'oktauser',
|
||||
target: 'hackeruser',
|
||||
label: 'CreateUser',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(oktauser)-b(s3)',
|
||||
source: 'oktauser',
|
||||
target: 's3',
|
||||
label: 'PutObject',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(oktauser)-b(ec2)',
|
||||
source: 'oktauser',
|
||||
target: 'ec2',
|
||||
label: 'RunInstances',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(oktauser)-b(aws)',
|
||||
source: 'oktauser',
|
||||
target: 'aws',
|
||||
label: 'DeleteTrail (Failed)',
|
||||
color: 'warning',
|
||||
shape: 'label',
|
||||
},
|
||||
];
|
||||
|
||||
Graph.args = {
|
||||
...extractEdges(baseGraph),
|
||||
};
|
||||
|
||||
export const GraphLabelOverlayCases = Template.bind({});
|
||||
|
||||
GraphLabelOverlayCases.args = {
|
||||
...extractEdges([
|
||||
...baseGraph,
|
||||
{
|
||||
id: 'newnode',
|
||||
label: 'New Node',
|
||||
color: 'primary',
|
||||
shape: 'ellipse',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
id: 'a(newnode)-b(hackeruser)',
|
||||
source: 'newnode',
|
||||
target: 'hackeruser',
|
||||
label: 'Overlay Label',
|
||||
color: 'danger',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(newnode)-b(s3)',
|
||||
source: 'newnode',
|
||||
target: 's3',
|
||||
label: 'Overlay Label',
|
||||
color: 'danger',
|
||||
shape: 'label',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
export const GraphStackedEdgeCases = Template.bind({});
|
||||
|
||||
GraphStackedEdgeCases.args = {
|
||||
...extractEdges([
|
||||
...baseGraph,
|
||||
{
|
||||
id: 'a(oktauser)-b(hackeruser)',
|
||||
source: 'oktauser',
|
||||
target: 'hackeruser',
|
||||
label: 'CreateUser2',
|
||||
color: 'primary',
|
||||
shape: 'label',
|
||||
},
|
||||
{
|
||||
id: 'a(siem-windows)-b(user)',
|
||||
source: 'siem-windows',
|
||||
target: 'user',
|
||||
label: 'User login to OKTA2',
|
||||
color: 'danger',
|
||||
shape: 'label',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
function processGraph(
|
||||
nodesModel: NodeDataModel[],
|
||||
edgesModel: EdgeDataModel[]
|
||||
): {
|
||||
initialNodes: Node[];
|
||||
initialEdges: Edge[];
|
||||
} {
|
||||
const { nodes: nodesViewModel } = layoutGraph(nodesModel, edgesModel);
|
||||
|
||||
const nodesById: { [key: string]: NodeViewModel } = {};
|
||||
|
||||
const initialNodes = nodesViewModel.map((nodeData) => {
|
||||
nodesById[nodeData.id] = nodeData;
|
||||
|
||||
const node: Node = {
|
||||
id: nodeData.id,
|
||||
type: nodeData.shape,
|
||||
data: { ...nodeData, interactive: true },
|
||||
position: nodeData.position,
|
||||
draggable: true,
|
||||
};
|
||||
|
||||
if (node.type === 'group' && nodeData.shape === 'group') {
|
||||
node.sourcePosition = Position.Right;
|
||||
node.targetPosition = Position.Left;
|
||||
node.resizing = false;
|
||||
node.style = GroupStyleOverride({
|
||||
width: nodeData.size?.width ?? 0,
|
||||
height: nodeData.size?.height ?? 0,
|
||||
});
|
||||
} else if (nodeData.shape === 'label' && nodeData.parentId) {
|
||||
node.parentId = nodeData.parentId;
|
||||
node.extent = 'parent';
|
||||
node.expandParent = false;
|
||||
node.draggable = false;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
const initialEdges: Edge[] = 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,
|
||||
data: { ...edgeData },
|
||||
};
|
||||
});
|
||||
|
||||
return { initialNodes, initialEdges };
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { ThemeProvider } from '@emotion/react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Controls,
|
||||
Background,
|
||||
Position,
|
||||
Handle,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
type BuiltInNode,
|
||||
type NodeProps,
|
||||
} from '@xyflow/react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { SvgDefsMarker } from './styles';
|
||||
import { DefaultEdge } from '.';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { LabelNode } from '../node';
|
||||
import type { NodeViewModel } from '../types';
|
||||
|
||||
export default {
|
||||
title: 'Components/Graph Components/Default Edge',
|
||||
description: 'CDR - Graph visualization',
|
||||
argTypes: {
|
||||
color: {
|
||||
options: ['primary', 'danger', 'warning'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nodeTypes = {
|
||||
default: ((props: NodeProps<BuiltInNode>) => {
|
||||
const handleStyle = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
'min-width': 0,
|
||||
'min-height': 0,
|
||||
border: 'none',
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Handle type="source" position={Position.Right} style={handleStyle} />
|
||||
<Handle type="target" position={Position.Left} style={handleStyle} />
|
||||
{props.data.label}
|
||||
</div>
|
||||
);
|
||||
}) as React.FC<NodeProps<BuiltInNode>>,
|
||||
label: LabelNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
default: DefaultEdge,
|
||||
};
|
||||
|
||||
const Template: Story<NodeViewModel> = (args: NodeViewModel) => {
|
||||
const initialNodes = [
|
||||
{
|
||||
id: 'source',
|
||||
type: 'default',
|
||||
data: { label: 'source' },
|
||||
position: { x: 0, y: 0 },
|
||||
draggable: true,
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
type: 'default',
|
||||
data: { label: 'target' },
|
||||
position: { x: 320, y: 100 },
|
||||
draggable: true,
|
||||
},
|
||||
{
|
||||
id: args.id,
|
||||
type: 'label',
|
||||
data: args,
|
||||
position: { x: 160, y: 50 },
|
||||
draggable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges = [
|
||||
{
|
||||
id: 'source-' + args.id,
|
||||
source: 'source',
|
||||
target: args.id,
|
||||
data: {
|
||||
id: 'source-' + args.id,
|
||||
source: 'source',
|
||||
sourceShape: 'rectangle',
|
||||
target: args.id,
|
||||
targetShape: 'label',
|
||||
color: args.color,
|
||||
interactive: true,
|
||||
},
|
||||
type: 'default',
|
||||
},
|
||||
{
|
||||
id: args.id + '-target',
|
||||
source: args.id,
|
||||
target: 'target',
|
||||
data: {
|
||||
id: args.id + '-target',
|
||||
source: args.id,
|
||||
sourceShape: 'label',
|
||||
target: 'target',
|
||||
targetShape: 'rectangle',
|
||||
color: args.color,
|
||||
interactive: true,
|
||||
},
|
||||
type: 'default',
|
||||
},
|
||||
];
|
||||
|
||||
const [nodes, _setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, _setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={{ darkMode: false }}>
|
||||
<SvgDefsMarker />
|
||||
<ReactFlow
|
||||
fitView
|
||||
attributionPosition={undefined}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Edge = Template.bind({});
|
||||
|
||||
Edge.args = {
|
||||
id: 'siem-windows',
|
||||
label: 'User login to OKTA',
|
||||
color: 'primary',
|
||||
icon: 'okta',
|
||||
interactive: true,
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { BaseEdge, getBezierPath } from '@xyflow/react';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import type { Color } from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
import type { EdgeProps } from '../types';
|
||||
import { getMarker } from './styles';
|
||||
import { getShapeHandlePosition } from './utils';
|
||||
|
||||
export function DefaultEdge({
|
||||
id,
|
||||
label,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
data,
|
||||
}: EdgeProps) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const color: Color = data?.color ?? 'primary';
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
// sourceX and targetX are adjusted to account for the shape handle position
|
||||
sourceX: sourceX - getShapeHandlePosition(data?.sourceShape),
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX: targetX + getShapeHandlePosition(data?.targetShape),
|
||||
targetY,
|
||||
targetPosition,
|
||||
curvature:
|
||||
0.1 *
|
||||
(data?.sourceShape === 'group' ||
|
||||
(data?.sourceShape === 'label' && data?.targetShape === 'group')
|
||||
? -1 // We flip direction when the edge is between parent node to child nodes (groups always contain children in our graph)
|
||||
: 1),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
style={{
|
||||
stroke: euiTheme.colors[color],
|
||||
strokeDasharray: '2,2',
|
||||
}}
|
||||
markerEnd={
|
||||
data?.targetShape !== 'label' && data?.targetShape !== 'group'
|
||||
? getMarker(color)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { DefaultEdge } from './default_edge';
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 styled from '@emotion/styled';
|
||||
import { rgba } from 'polished';
|
||||
import {
|
||||
useEuiTheme,
|
||||
useEuiBackgroundColor,
|
||||
EuiText,
|
||||
type EuiTextProps,
|
||||
type _EuiBackgroundColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const EdgeLabelHeight = 24;
|
||||
export const EdgeLabelWidth = 100;
|
||||
|
||||
export interface EdgeLabelContainerProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export const EdgeLabelContainer = styled.div<EdgeLabelContainerProps>`
|
||||
position: absolute;
|
||||
${(props) =>
|
||||
props.scale && 0 < props.scale && props.scale < 1
|
||||
? `transform: scale(${props.scale}) translateX(${(1 - props.scale) * 50}%)`
|
||||
: ''};
|
||||
width: ${(props) => props.width ?? EdgeLabelWidth}px;
|
||||
height: ${(props) => props.height ?? EdgeLabelHeight}px;
|
||||
// Everything inside EdgeLabelRenderer has no pointer events by default
|
||||
// To have an interactive element, set pointer-events: all
|
||||
pointer-events: all;
|
||||
text-wrap: nowrap;
|
||||
`;
|
||||
|
||||
export interface EdgeLabelProps extends EuiTextProps {
|
||||
labelX?: number;
|
||||
labelY?: number;
|
||||
}
|
||||
|
||||
export const EdgeLabel = styled(EuiText)<EdgeLabelProps>`
|
||||
position: absolute;
|
||||
transform: ${(props) =>
|
||||
`translate(-50%, -50%)${
|
||||
props.labelX && props.labelY ? ` translate(${props.labelX}px,${props.labelY}px)` : ''
|
||||
}`};
|
||||
background: ${(props) => useEuiBackgroundColor(props.color as _EuiBackgroundColor)};
|
||||
border: ${(props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `solid ${euiTheme.colors[props.color as keyof typeof euiTheme.colors]} 1px`;
|
||||
}};
|
||||
font-weight: ${(_props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `${euiTheme.font.weight.semiBold}`;
|
||||
}};
|
||||
font-size: ${(_props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `${euiTheme.font.scale.xs * 10.5}px`;
|
||||
}};
|
||||
padding: 0px 2px;
|
||||
border-radius: 16px;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
`;
|
||||
|
||||
export const EdgeLabelOnHover = styled(EdgeLabel)<EdgeLabelProps & EdgeLabelContainerProps>`
|
||||
opacity: 0; /* Hidden by default */
|
||||
transition: opacity 0.2s ease; /* Smooth transition */
|
||||
border: ${(props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `dashed ${rgba(
|
||||
euiTheme.colors[props.color as keyof typeof euiTheme.colors] as string,
|
||||
0.5
|
||||
)} 1px`;
|
||||
}};
|
||||
border-radius: 20px;
|
||||
width: ${(props) => (props.width ?? EdgeLabelWidth) + 10}px;
|
||||
height: ${(props) => (props.height ?? EdgeLabelHeight) + 10}px;
|
||||
background: transparent;
|
||||
|
||||
${EdgeLabelContainer}:hover & {
|
||||
opacity: 1; /* Show on hover */
|
||||
}
|
||||
`;
|
||||
|
||||
const Marker = ({ id, color }: { id: string; color: string }) => {
|
||||
return (
|
||||
<marker
|
||||
id={id}
|
||||
markerWidth="12"
|
||||
markerHeight="12"
|
||||
viewBox="-10 -10 20 20"
|
||||
markerUnits="strokeWidth"
|
||||
orient="auto-start-reverse"
|
||||
refX="0"
|
||||
refY="0"
|
||||
>
|
||||
<polyline
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
points="-5,-4 0,0 -5,4 -5,-4"
|
||||
strokeWidth="1"
|
||||
stroke={color}
|
||||
fill={color}
|
||||
/>
|
||||
</marker>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarkerType = {
|
||||
primary: 'url(#primary)',
|
||||
danger: 'url(#danger)',
|
||||
warning: 'url(#warning)',
|
||||
};
|
||||
|
||||
export const getMarker = (color: string) => {
|
||||
const colorKey = color as keyof typeof MarkerType;
|
||||
return MarkerType[colorKey] ?? MarkerType.primary;
|
||||
};
|
||||
|
||||
export const SvgDefsMarker = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<svg style={{ position: 'absolute', top: 0, left: 0 }}>
|
||||
<defs>
|
||||
<Marker id="primary" color={euiTheme.colors.primary} />
|
||||
<Marker id="danger" color={euiTheme.colors.danger} />
|
||||
<Marker id="warning" color={euiTheme.colors.warning} />
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 type { NodeShape } from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
|
||||
export function getShapeHandlePosition(shape?: NodeShape) {
|
||||
switch (shape) {
|
||||
case 'hexagon':
|
||||
return 14;
|
||||
case 'pentagon':
|
||||
return 14;
|
||||
case 'ellipse':
|
||||
return 13;
|
||||
case 'rectangle':
|
||||
return 16;
|
||||
case 'diamond':
|
||||
return 10;
|
||||
case 'label':
|
||||
return 3;
|
||||
case 'group':
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 Dagre from '@dagrejs/dagre';
|
||||
import type {
|
||||
EdgeDataModel,
|
||||
NodeDataModel,
|
||||
} from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
import type { NodeViewModel, Size } from '../types';
|
||||
import { calcLabelSize } from './utils';
|
||||
|
||||
export const layoutGraph = (
|
||||
nodes: NodeDataModel[],
|
||||
edges: EdgeDataModel[]
|
||||
): { nodes: NodeViewModel[] } => {
|
||||
const nodesById: { [key: string]: NodeViewModel } = {};
|
||||
const graphOpts = {
|
||||
compound: true,
|
||||
};
|
||||
|
||||
const g = new Dagre.graphlib.Graph(graphOpts)
|
||||
.setGraph({ rankdir: 'LR', align: 'UL' })
|
||||
.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
||||
|
||||
nodes.forEach((node) => {
|
||||
let size = { width: 90, height: 90 };
|
||||
const position = { x: 0, y: 0 };
|
||||
|
||||
if (node.shape === 'label') {
|
||||
size = calcLabelSize(node.label);
|
||||
|
||||
// TODO: waiting for a fix: https://github.com/dagrejs/dagre/issues/238
|
||||
// if (node.parentId) {
|
||||
// g.setParent(node.id, node.parentId);
|
||||
// }
|
||||
} else if (node.shape === 'group') {
|
||||
const res = layoutGroupChildren(node, nodes);
|
||||
|
||||
size = res.size;
|
||||
|
||||
res.children.forEach((child) => {
|
||||
nodesById[child.id] = { ...child };
|
||||
});
|
||||
}
|
||||
|
||||
if (!nodesById[node.id]) {
|
||||
nodesById[node.id] = { ...node, position };
|
||||
}
|
||||
|
||||
g.setNode(node.id, {
|
||||
...node,
|
||||
...size,
|
||||
});
|
||||
});
|
||||
|
||||
Dagre.layout(g);
|
||||
|
||||
const nodesViewModel: NodeViewModel[] = nodes.map((nodeData) => {
|
||||
const dagreNode = g.node(nodeData.id);
|
||||
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the React Flow node anchor point (top left).
|
||||
const x = dagreNode.x - (dagreNode.width ?? 0) / 2;
|
||||
const y = dagreNode.y - (dagreNode.height ?? 0) / 2;
|
||||
|
||||
// For grouped nodes, we want to keep the original position relative to the parent
|
||||
if (nodeData.shape === 'label' && nodeData.parentId) {
|
||||
return {
|
||||
...nodeData,
|
||||
position: nodesById[nodeData.id].position,
|
||||
};
|
||||
} else if (nodeData.shape === 'group') {
|
||||
return {
|
||||
...nodeData,
|
||||
position: { x, y },
|
||||
size: {
|
||||
width: dagreNode.width,
|
||||
height: dagreNode.height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...nodeData,
|
||||
position: { x, y },
|
||||
};
|
||||
});
|
||||
|
||||
return { nodes: nodesViewModel };
|
||||
};
|
||||
|
||||
const layoutGroupChildren = (
|
||||
groupNode: NodeDataModel,
|
||||
nodes: NodeDataModel[]
|
||||
): { size: Size; children: NodeViewModel[] } => {
|
||||
const children = nodes.filter(
|
||||
(child) => child.shape === 'label' && child.parentId === groupNode.id
|
||||
);
|
||||
|
||||
const STACK_VERTICAL_PADDING = 20;
|
||||
const MIN_STACK_HEIGHT = 70;
|
||||
const PADDING = 20;
|
||||
const stackSize = children.length;
|
||||
const allChildrenHeight = children.reduce(
|
||||
(prevHeight, node) => prevHeight + calcLabelSize(node.label).height,
|
||||
0
|
||||
);
|
||||
const stackHeight = Math.max(
|
||||
allChildrenHeight + (stackSize - 1) * STACK_VERTICAL_PADDING,
|
||||
MIN_STACK_HEIGHT
|
||||
);
|
||||
|
||||
const space = (stackHeight - allChildrenHeight) / (stackSize - 1);
|
||||
const groupNodeWidth = children.reduce((acc, child) => {
|
||||
const currLblWidth = PADDING * 2 + calcLabelSize(child.label).width;
|
||||
return Math.max(acc, currLblWidth);
|
||||
}, 0);
|
||||
|
||||
// Layout children relative to parent
|
||||
const positionedChildren: NodeViewModel[] = children.map((child, index) => {
|
||||
const childSize = calcLabelSize(child.label);
|
||||
const childPosition = {
|
||||
x: groupNodeWidth / 2 - childSize.width / 2,
|
||||
y: index * (childSize.height * 2 + space),
|
||||
};
|
||||
|
||||
return { ...child, position: childPosition };
|
||||
});
|
||||
|
||||
return {
|
||||
size: { width: groupNodeWidth, height: stackHeight },
|
||||
children: positionedChildren,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { EdgeLabelHeight, EdgeLabelWidth } from '../edge/styles';
|
||||
import { LABEL_BORDER_WIDTH, LABEL_PADDING_X } from '../node/styles';
|
||||
|
||||
const LABEL_FONT = `600 7.875px Inter, "system-ui", Helvetica, Arial, sans-serif`;
|
||||
const LABEL_PADDING = (LABEL_PADDING_X + LABEL_BORDER_WIDTH) * 2;
|
||||
|
||||
export const calcLabelSize = (label?: string) => {
|
||||
const currLblWidth = Math.max(EdgeLabelWidth, LABEL_PADDING + getTextWidth(label ?? ''));
|
||||
return { width: currLblWidth, height: EdgeLabelHeight };
|
||||
};
|
||||
|
||||
interface GetTextWidth {
|
||||
(text: string, font?: string): number;
|
||||
|
||||
// static canvas element for measuring text width
|
||||
canvas?: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
export const getTextWidth: GetTextWidth = (text: string, font: string = LABEL_FONT) => {
|
||||
// re-use canvas object for better performance
|
||||
const canvas: HTMLCanvasElement =
|
||||
getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas'));
|
||||
const context = canvas.getContext('2d');
|
||||
if (context) {
|
||||
context.font = font;
|
||||
}
|
||||
const metrics = context?.measureText(text);
|
||||
return metrics?.width ?? 0;
|
||||
};
|
||||
|
||||
function getCssStyle(element: HTMLElement, prop: string) {
|
||||
return window.getComputedStyle(element, null).getPropertyValue(prop);
|
||||
}
|
||||
|
||||
// @ts-ignore will use it to get the font of the canvas on runtime
|
||||
function getCanvasFont(el = document.body) {
|
||||
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
|
||||
const fontSize = getCssStyle(el, 'font-size') || '16px';
|
||||
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
|
||||
|
||||
return `${fontWeight} ${fontSize} ${fontFamily}`;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { ThemeProvider } from '@emotion/react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { NodeButton, type NodeButtonProps, NodeContainer } from './styles';
|
||||
|
||||
export default {
|
||||
title: 'Components/Graph Components',
|
||||
description: 'CDR - Graph visualization',
|
||||
argTypes: {
|
||||
onClick: { action: 'onClick' },
|
||||
},
|
||||
};
|
||||
|
||||
const Template: Story<NodeButtonProps> = (args) => (
|
||||
<ThemeProvider theme={{ darkMode: false }}>
|
||||
<NodeContainer>
|
||||
Hover me
|
||||
<NodeButton onClick={args.onClick} />
|
||||
</NodeContainer>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export const Button = Template.bind({});
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, NodeResizeControl, Position } from '@xyflow/react';
|
||||
import { HandleStyleOverride } from './styles';
|
||||
import type { NodeProps } from '../types';
|
||||
|
||||
export const EdgeGroupNode: React.FC<NodeProps> = memo((props: NodeProps) => {
|
||||
// Handles order horizontally is: in > inside > out > outside
|
||||
return (
|
||||
<>
|
||||
<NodeResizeControl
|
||||
position="right"
|
||||
style={{ borderColor: 'transparent', background: 'transparent' }}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
id="out"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
id="outside"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
</NodeResizeControl>
|
||||
<Handle
|
||||
type="source"
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
id="inside"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
id="in"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import {
|
||||
NodeContainer,
|
||||
NodeLabel,
|
||||
NodeShapeOnHoverSvg,
|
||||
NodeShapeSvg,
|
||||
NodeIcon,
|
||||
NodeButton,
|
||||
HandleStyleOverride,
|
||||
} from './styles';
|
||||
import type { EntityNodeViewModel, NodeProps } from '../types';
|
||||
|
||||
const NODE_WIDTH = 90;
|
||||
const NODE_HEIGHT = 90;
|
||||
|
||||
export const EllipseNode: React.FC<NodeProps> = memo((props: NodeProps) => {
|
||||
const { id, color, icon, label, interactive, expandButtonClick } =
|
||||
props.data as EntityNodeViewModel;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<NodeContainer>
|
||||
{interactive && (
|
||||
<NodeShapeOnHoverSvg
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
viewBox={`0 0 ${NODE_WIDTH} ${NODE_HEIGHT}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
opacity="0.5"
|
||||
cx="45"
|
||||
cy="45"
|
||||
r="44.5"
|
||||
stroke={euiTheme.colors[color ?? 'primary']}
|
||||
strokeDasharray="2 2"
|
||||
/>
|
||||
</NodeShapeOnHoverSvg>
|
||||
)}
|
||||
<NodeShapeSvg
|
||||
width="72"
|
||||
height="72"
|
||||
viewBox="0 0 72 72"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="36"
|
||||
cy="36"
|
||||
r="35.5"
|
||||
fill={useEuiBackgroundColor(color ?? 'primary')}
|
||||
stroke={euiTheme.colors[color ?? 'primary']}
|
||||
/>
|
||||
{icon && <NodeIcon x="11" y="12" icon={icon} color={color} />}
|
||||
</NodeShapeSvg>
|
||||
{interactive && (
|
||||
<NodeButton
|
||||
onClick={(e) => expandButtonClick?.(e, props)}
|
||||
x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 2}px`}
|
||||
y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize) / 2}px`}
|
||||
/>
|
||||
)}
|
||||
<Handle
|
||||
type="target"
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
id="in"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
id="out"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<NodeLabel>{Boolean(label) ? label : id}</NodeLabel>
|
||||
</NodeContainer>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 awsIcon from '../../assets/icons/aws.svg';
|
||||
import awsEc2Icon from '../../assets/icons/aws_ec2.svg';
|
||||
import awsS3Icon from '../../assets/icons/aws_s3.svg';
|
||||
import oktaIcon from '../../assets/icons/okta.svg';
|
||||
|
||||
const icons: Record<string, any> = {
|
||||
aws: awsIcon,
|
||||
aws_ec2: awsEc2Icon,
|
||||
aws_s3: awsS3Icon,
|
||||
okta: oktaIcon,
|
||||
};
|
||||
|
||||
export function getSpanIcon(type?: string) {
|
||||
return icons[type ?? ''];
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { DiamondNode } from './diamond_node';
|
||||
export { EllipseNode } from './ellipse_node';
|
||||
export { HexagonNode } from './hexagon_node';
|
||||
export { PentagonNode } from './pentagon_node';
|
||||
export { RectangleNode } from './rectangle_node';
|
||||
export { LabelNode } from './label_node';
|
||||
export { EdgeGroupNode } from './edge_group_node';
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { LabelNodeContainer, LabelShape, HandleStyleOverride, LabelShapeOnHover } from './styles';
|
||||
import type { LabelNodeViewModel, NodeProps } from '../types';
|
||||
|
||||
export const LabelNode: React.FC<NodeProps> = memo((props: NodeProps) => {
|
||||
const { id, color, label, interactive } = props.data as LabelNodeViewModel;
|
||||
|
||||
return (
|
||||
<LabelNodeContainer>
|
||||
{interactive && <LabelShapeOnHover color={color} />}
|
||||
<LabelShape color={color} textAlign="center">
|
||||
{Boolean(label) ? label : id}
|
||||
</LabelShape>
|
||||
<Handle
|
||||
type="target"
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
id="in"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
id="out"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
</LabelNodeContainer>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { ThemeProvider } from '@emotion/react';
|
||||
import { pick } from 'lodash';
|
||||
import { ReactFlow, Controls, Background } from '@xyflow/react';
|
||||
import { Story } from '@storybook/react';
|
||||
import { NodeViewModel } from '../types';
|
||||
import { HexagonNode, PentagonNode, EllipseNode, RectangleNode, DiamondNode, LabelNode } from '.';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
export default {
|
||||
title: 'Components/Graph Components',
|
||||
description: 'CDR - Graph visualization',
|
||||
argTypes: {
|
||||
color: {
|
||||
options: ['primary', 'danger', 'warning'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
shape: {
|
||||
options: ['ellipse', 'hexagon', 'pentagon', 'rectangle', 'diamond', 'label'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
expandButtonClick: { action: 'expandButtonClick' },
|
||||
},
|
||||
};
|
||||
|
||||
const nodeTypes = {
|
||||
hexagon: HexagonNode,
|
||||
pentagon: PentagonNode,
|
||||
ellipse: EllipseNode,
|
||||
rectangle: RectangleNode,
|
||||
diamond: DiamondNode,
|
||||
label: LabelNode,
|
||||
};
|
||||
|
||||
const Template: Story<NodeViewModel> = (args: NodeViewModel) => (
|
||||
<ThemeProvider theme={{ darkMode: false }}>
|
||||
<ReactFlow
|
||||
fitView
|
||||
attributionPosition={undefined}
|
||||
nodeTypes={nodeTypes}
|
||||
nodes={[
|
||||
{
|
||||
id: args.id,
|
||||
type: args.shape,
|
||||
data: pick(args, ['id', 'label', 'color', 'icon', 'interactive', 'expandButtonClick']),
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export const Node = Template.bind({});
|
||||
|
||||
Node.args = {
|
||||
id: 'siem-windows',
|
||||
label: '',
|
||||
color: 'primary',
|
||||
shape: 'hexagon',
|
||||
icon: 'okta',
|
||||
interactive: true,
|
||||
};
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import {
|
||||
NodeContainer,
|
||||
NodeLabel,
|
||||
NodeShapeOnHoverSvg,
|
||||
NodeShapeSvg,
|
||||
NodeIcon,
|
||||
NodeButton,
|
||||
HandleStyleOverride,
|
||||
} from './styles';
|
||||
import type { EntityNodeViewModel, NodeProps } from '../types';
|
||||
|
||||
const NODE_WIDTH = 81;
|
||||
const NODE_HEIGHT = 80;
|
||||
|
||||
export const RectangleNode: React.FC<NodeProps> = memo((props: NodeProps) => {
|
||||
const { id, color, icon, label, interactive, expandButtonClick } =
|
||||
props.data as EntityNodeViewModel;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<NodeContainer>
|
||||
{interactive && (
|
||||
<NodeShapeOnHoverSvg
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
viewBox={`0 0 ${NODE_WIDTH} ${NODE_HEIGHT}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
opacity="0.5"
|
||||
x="1"
|
||||
y="0.5"
|
||||
width="79"
|
||||
height="79"
|
||||
rx="7.5"
|
||||
stroke={euiTheme.colors[color ?? 'primary']}
|
||||
strokeDasharray="2 2"
|
||||
/>
|
||||
</NodeShapeOnHoverSvg>
|
||||
)}
|
||||
<NodeShapeSvg
|
||||
width="65"
|
||||
height="64"
|
||||
viewBox="0 0 65 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="1"
|
||||
y="0.5"
|
||||
width="63"
|
||||
height="63"
|
||||
rx="7.5"
|
||||
fill={useEuiBackgroundColor(color ?? 'primary')}
|
||||
stroke={euiTheme.colors[color ?? 'primary']}
|
||||
/>
|
||||
{icon && <NodeIcon x="8" y="7" icon={icon} color={color} />}
|
||||
</NodeShapeSvg>
|
||||
{interactive && (
|
||||
<NodeButton
|
||||
onClick={(e) => expandButtonClick?.(e, props)}
|
||||
x={`${NODE_WIDTH - NodeButton.ExpandButtonSize / 4}px`}
|
||||
y={`${(NODE_HEIGHT - NodeButton.ExpandButtonSize / 2) / 2}px`}
|
||||
/>
|
||||
)}
|
||||
<Handle
|
||||
type="target"
|
||||
isConnectable={false}
|
||||
position={Position.Left}
|
||||
id="in"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
isConnectable={false}
|
||||
position={Position.Right}
|
||||
id="out"
|
||||
style={HandleStyleOverride}
|
||||
/>
|
||||
<NodeLabel>{Boolean(label) ? label : id}</NodeLabel>
|
||||
</NodeContainer>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
type EuiIconProps,
|
||||
type _EuiBackgroundColor,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
useEuiBackgroundColor,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { rgba } from 'polished';
|
||||
import { getSpanIcon } from './get_span_icon';
|
||||
|
||||
export const LABEL_PADDING_X = 15;
|
||||
export const LABEL_BORDER_WIDTH = 1;
|
||||
export const NODE_WIDTH = 90;
|
||||
export const NODE_HEIGHT = 90;
|
||||
|
||||
export const LabelNodeContainer = styled.div`
|
||||
text-wrap: nowrap;
|
||||
min-width: 100px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export const LabelShape = styled(EuiText)`
|
||||
background: ${(props) => useEuiBackgroundColor(props.color as _EuiBackgroundColor)};
|
||||
border: ${(props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `solid ${
|
||||
euiTheme.colors[props.color as keyof typeof euiTheme.colors]
|
||||
} ${LABEL_BORDER_WIDTH}px`;
|
||||
}};
|
||||
|
||||
font-weight: ${(_props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `${euiTheme.font.weight.semiBold}`;
|
||||
}};
|
||||
font-size: ${(_props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `${euiTheme.font.scale.xs * 10.5}px`;
|
||||
}};
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
padding: 5px ${LABEL_PADDING_X}px;
|
||||
border-radius: 16px;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
`;
|
||||
|
||||
export const LabelShapeOnHover = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
opacity: 0; /* Hidden by default */
|
||||
transition: opacity 0.2s ease; /* Smooth transition */
|
||||
border: ${(props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return `dashed ${rgba(
|
||||
euiTheme.colors[props.color as keyof typeof euiTheme.colors] as string,
|
||||
0.5
|
||||
)} 1px`;
|
||||
}};
|
||||
border-radius: 20px;
|
||||
background: transparent;
|
||||
width: 108%;
|
||||
height: 134%;
|
||||
|
||||
${LabelNodeContainer}:hover & {
|
||||
opacity: 1; /* Show on hover */
|
||||
}
|
||||
`;
|
||||
|
||||
export const NodeContainer = styled.div`
|
||||
position: relative;
|
||||
width: ${NODE_WIDTH}px;
|
||||
height: ${NODE_HEIGHT}px;
|
||||
`;
|
||||
|
||||
export const NodeShapeSvg = styled.svg`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const NodeShapeOnHoverSvg = styled(NodeShapeSvg)`
|
||||
opacity: 0; /* Hidden by default */
|
||||
transition: opacity 0.2s ease; /* Smooth transition */
|
||||
|
||||
${NodeContainer}:hover & {
|
||||
opacity: 1; /* Show on hover */
|
||||
}
|
||||
`;
|
||||
|
||||
interface NodeIconProps {
|
||||
icon: string;
|
||||
color?: EuiIconProps['color'];
|
||||
x: string;
|
||||
y: string;
|
||||
}
|
||||
|
||||
export const NodeIcon = ({ icon, color, x, y }: NodeIconProps) => {
|
||||
return (
|
||||
<foreignObject x={x} y={y} width="50" height="50">
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}
|
||||
>
|
||||
<EuiIcon type={getSpanIcon(icon) ?? icon} size="l" color={color ?? 'primary'} />
|
||||
</div>
|
||||
</foreignObject>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeLabel = styled(EuiText)`
|
||||
position: absolute;
|
||||
top: 108%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 130%;
|
||||
text-overflow: ellipsis;
|
||||
// white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
NodeLabel.defaultProps = {
|
||||
size: 'xs',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const ExpandButtonSize = 18;
|
||||
|
||||
const RoundEuiButtonIcon = styled(EuiButtonIcon)`
|
||||
border-radius: 50%;
|
||||
background-color: ${(_props) => useEuiBackgroundColor('plain')};
|
||||
width: ${ExpandButtonSize}px;
|
||||
height: ${ExpandButtonSize}px;
|
||||
|
||||
> svg {
|
||||
transform: translate(0.75px, 0.75px);
|
||||
}
|
||||
|
||||
:hover,
|
||||
:focus,
|
||||
:active {
|
||||
background-color: ${(_props) => useEuiBackgroundColor('plain')};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledNodeButton = styled.div<NodeButtonProps>`
|
||||
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;
|
||||
|
||||
${NodeContainer}:hover & {
|
||||
opacity: 1; /* Show on hover */
|
||||
}
|
||||
`;
|
||||
|
||||
export interface NodeButtonProps {
|
||||
x?: string;
|
||||
y?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLElement>) => 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<HTMLElement>) => {
|
||||
setIsToggled(!isToggled);
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledNodeButton x={x} y={y}>
|
||||
<RoundEuiButtonIcon
|
||||
color="primary"
|
||||
iconType={isToggled ? 'minusInCircleFilled' : 'plusInCircleFilled'}
|
||||
onClick={onClickHandler}
|
||||
iconSize="m"
|
||||
/>
|
||||
</StyledNodeButton>
|
||||
);
|
||||
};
|
||||
|
||||
NodeButton.ExpandButtonSize = ExpandButtonSize;
|
||||
|
||||
export const HandleStyleOverride: React.CSSProperties = {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
export const GroupStyleOverride = (size?: {
|
||||
width: number;
|
||||
height: number;
|
||||
}): React.CSSProperties => ({
|
||||
backgroundColor: 'transparent',
|
||||
border: '0px solid',
|
||||
boxShadow: 'none',
|
||||
width: size?.width ?? 140,
|
||||
height: size?.height ?? 75,
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 type {
|
||||
EntityNodeDataModel,
|
||||
GroupNodeDataModel,
|
||||
LabelNodeDataModel,
|
||||
EdgeDataModel,
|
||||
} from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
import type { Node, NodeProps as xyNodeProps } from '@xyflow/react';
|
||||
import type { Edge, EdgeProps as xyEdgeProps } from '@xyflow/react';
|
||||
|
||||
export interface PositionXY {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface GraphMetadata {
|
||||
nodes: { [key: string]: { edgesIn: number; edgesOut: number } };
|
||||
edges: {
|
||||
[key: string]: { source: string; target: string; edgesStacked: number; edges: string[] };
|
||||
};
|
||||
}
|
||||
|
||||
interface BaseNodeDataViewModel {
|
||||
position: PositionXY;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityNodeViewModel
|
||||
extends Record<string, unknown>,
|
||||
EntityNodeDataModel,
|
||||
BaseNodeDataViewModel {
|
||||
expandButtonClick?: (e: React.MouseEvent<HTMLElement>, node: NodeProps) => void;
|
||||
}
|
||||
|
||||
export interface GroupNodeViewModel
|
||||
extends Record<string, unknown>,
|
||||
GroupNodeDataModel,
|
||||
BaseNodeDataViewModel {
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export interface LabelNodeViewModel
|
||||
extends Record<string, unknown>,
|
||||
LabelNodeDataModel,
|
||||
BaseNodeDataViewModel {
|
||||
expandButtonClick?: (e: React.MouseEvent<HTMLElement>, node: NodeProps) => void;
|
||||
}
|
||||
|
||||
export type NodeViewModel = EntityNodeViewModel | GroupNodeViewModel | LabelNodeViewModel;
|
||||
|
||||
export type NodeProps = xyNodeProps<Node<NodeViewModel>>;
|
||||
|
||||
export interface EdgeViewModel extends Record<string, unknown>, EdgeDataModel {
|
||||
graphMetadata?: GraphMetadata;
|
||||
interactive?: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLElement>, edge: EdgeProps) => void;
|
||||
}
|
||||
|
||||
export type EdgeProps = xyEdgeProps<Edge<EdgeViewModel>>;
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/cloud-security-posture-common",
|
||||
"@kbn/utility-types"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/** The title of the Storybook. */
|
||||
export const TITLE = 'Cloud Security Posture Storybook';
|
||||
|
||||
/** The remote URL of the root from which Storybook loads stories for Cloud Security Solution. */
|
||||
export const URL =
|
||||
'https://github.com/elastic/kibana/tree/main/x-pack/packages/kbn-cloud-security-posture';
|
8
x-pack/packages/kbn-cloud-security-posture/storybook/config/index.ts
Executable file
8
x-pack/packages/kbn-cloud-security-posture/storybook/config/index.ts
Executable file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { TITLE, URL } from './constants';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { defaultConfig } from '@kbn/storybook';
|
||||
import { Configuration } from 'webpack';
|
||||
|
||||
module.exports = {
|
||||
...defaultConfig,
|
||||
stories: ['../../**/*.stories.+(tsx|mdx)'],
|
||||
reactOptions: {
|
||||
strictMode: true,
|
||||
},
|
||||
webpack: (config: Configuration) => {
|
||||
config.module?.rules.push({
|
||||
test: /\.js$/,
|
||||
include: /node_modules[\\\/]@dagrejs/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
},
|
||||
},
|
||||
});
|
||||
config.module?.rules.push({
|
||||
test: /node_modules[\/\\]@?xyflow[\/\\].*.js$/,
|
||||
loaders: 'babel-loader',
|
||||
options: {
|
||||
presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react'],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { addons } from '@storybook/addons';
|
||||
import { create } from '@storybook/theming';
|
||||
import { PANEL_ID as selectedPanel } from '@storybook/addon-actions';
|
||||
|
||||
import { TITLE as brandTitle, URL as brandUrl } from './constants';
|
||||
|
||||
addons.setConfig({
|
||||
theme: create({
|
||||
base: 'light',
|
||||
brandTitle,
|
||||
brandUrl,
|
||||
}),
|
||||
selectedPanel,
|
||||
showPanel: true.valueOf,
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import './styles.css';
|
||||
|
||||
export const parameters = {};
|
|
@ -0,0 +1,7 @@
|
|||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
|
@ -2,12 +2,6 @@
|
|||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
@ -41,5 +35,6 @@
|
|||
"@kbn/ui-theme",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/rison",
|
||||
"@kbn/storybook",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@ export const BENCHMARKS_API_CURRENT_VERSION = '1';
|
|||
export const FIND_CSP_BENCHMARK_RULE_ROUTE_PATH = '/internal/cloud_security_posture/rules/_find';
|
||||
export const FIND_CSP_BENCHMARK_RULE_API_CURRENT_VERSION = '1';
|
||||
|
||||
export const GRAPH_ROUTE_PATH = '/internal/cloud_security_posture/graph';
|
||||
export const GRAPH_API_CURRENT_VERSION = '1';
|
||||
|
||||
export const CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH =
|
||||
'/internal/cloud_security_posture/rules/_bulk_action';
|
||||
export const CSP_BENCHMARK_RULES_BULK_ACTION_API_CURRENT_VERSION = '1';
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 {
|
||||
graphRequestSchema,
|
||||
graphResponseSchema,
|
||||
} from '@kbn/cloud-security-posture-common/schema/graph/latest';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { GRAPH_ROUTE_PATH } from '../../../common/constants';
|
||||
import { CspRouter } from '../../types';
|
||||
import { getGraph as getGraphV1 } from './v1';
|
||||
|
||||
export const defineGraphRoute = (router: CspRouter) =>
|
||||
router.versioned
|
||||
.post({
|
||||
access: 'internal',
|
||||
enableQueryVersion: true,
|
||||
path: GRAPH_ROUTE_PATH,
|
||||
options: {
|
||||
tags: ['access:cloud-security-posture-read'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: {
|
||||
request: {
|
||||
body: graphRequestSchema,
|
||||
},
|
||||
response: {
|
||||
200: { body: graphResponseSchema },
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { actorIds, eventIds, start, end } = request.body.query;
|
||||
const cspContext = await context.csp;
|
||||
const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id;
|
||||
|
||||
try {
|
||||
const { nodes, edges } = await getGraphV1(
|
||||
{
|
||||
logger: cspContext.logger,
|
||||
esClient: cspContext.esClient,
|
||||
},
|
||||
{
|
||||
actorIds,
|
||||
eventIds,
|
||||
spaceId,
|
||||
start,
|
||||
end,
|
||||
}
|
||||
);
|
||||
|
||||
return response.ok({ body: { nodes, edges } });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
cspContext.logger.error(`Failed to fetch graph ${err}`);
|
||||
cspContext.logger.error(err);
|
||||
return response.customError({
|
||||
body: { message: error.message },
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 type {
|
||||
EdgeDataModel,
|
||||
NodeDataModel,
|
||||
} from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
import type { Logger, IScopedClusterClient } from '@kbn/core/server';
|
||||
import type { Writable } from '@kbn/utility-types';
|
||||
|
||||
export interface GraphContextServices {
|
||||
logger: Logger;
|
||||
esClient: IScopedClusterClient;
|
||||
}
|
||||
|
||||
export interface GraphContext {
|
||||
nodes: Array<Writable<NodeDataModel>>;
|
||||
edges: Array<Writable<EdgeDataModel>>;
|
||||
}
|
353
x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts
Normal file
353
x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts
Normal file
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
* 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 { castArray } from 'lodash';
|
||||
import type { Logger, IScopedClusterClient } from '@kbn/core/server';
|
||||
import type {
|
||||
EdgeDataModel,
|
||||
NodeDataModel,
|
||||
EntityNodeDataModel,
|
||||
LabelNodeDataModel,
|
||||
GroupNodeDataModel,
|
||||
NodeShape,
|
||||
} from '@kbn/cloud-security-posture-common/types/graph/latest';
|
||||
import type { EsqlToRecords } from '@elastic/elasticsearch/lib/helpers';
|
||||
import type { Writeable } from '@kbn/zod';
|
||||
import type { GraphContextServices, GraphContext } from './types';
|
||||
|
||||
interface GraphEdge {
|
||||
badge: number;
|
||||
ips: string[];
|
||||
hosts: string[];
|
||||
users: string[];
|
||||
actorIds: string[] | string;
|
||||
action: string;
|
||||
targetIds: string[] | string;
|
||||
eventOutcome: string;
|
||||
isAlert: boolean;
|
||||
}
|
||||
|
||||
export const getGraph = async (
|
||||
services: GraphContextServices,
|
||||
query: {
|
||||
actorIds: string[];
|
||||
eventIds: string[];
|
||||
spaceId?: string;
|
||||
start: string | number;
|
||||
end: string | number;
|
||||
}
|
||||
): Promise<{
|
||||
nodes: NodeDataModel[];
|
||||
edges: EdgeDataModel[];
|
||||
}> => {
|
||||
const { esClient, logger } = services;
|
||||
const { actorIds, eventIds, spaceId = 'default', start, end } = query;
|
||||
|
||||
logger.trace(
|
||||
`Fetching graph for [eventIds: ${eventIds.join(', ')}] [actorIds: ${actorIds.join(
|
||||
', '
|
||||
)}] in [spaceId: ${spaceId}]`
|
||||
);
|
||||
|
||||
const results = await fetchGraph({ esClient, logger, start, end, eventIds, actorIds });
|
||||
|
||||
// Convert results into set of nodes and edges
|
||||
const graphContext = parseRecords(logger, results.records);
|
||||
|
||||
return { nodes: graphContext.nodes, edges: graphContext.edges };
|
||||
};
|
||||
|
||||
interface ParseContext {
|
||||
nodesMap: Record<string, NodeDataModel>;
|
||||
edgeLabelsNodes: Record<string, string[]>;
|
||||
edgesMap: Record<string, EdgeDataModel>;
|
||||
}
|
||||
|
||||
const parseRecords = (logger: Logger, records: GraphEdge[]): GraphContext => {
|
||||
const nodesMap: Record<string, NodeDataModel> = {};
|
||||
const edgeLabelsNodes: Record<string, string[]> = {};
|
||||
const edgesMap: Record<string, EdgeDataModel> = {};
|
||||
|
||||
logger.trace(`Parsing records [length: ${records.length}]`);
|
||||
|
||||
createNodes(logger, records, { nodesMap, edgeLabelsNodes });
|
||||
createEdgesAndGroups(logger, { edgeLabelsNodes, edgesMap, nodesMap });
|
||||
|
||||
logger.trace(
|
||||
`Parsed [nodes: ${Object.keys(nodesMap).length}, edges: ${Object.keys(edgesMap).length}]`
|
||||
);
|
||||
|
||||
// Sort groups to be first (fixes minor layout issue)
|
||||
const nodes = sortNodes(nodesMap);
|
||||
|
||||
return { nodes, edges: Object.values(edgesMap) };
|
||||
};
|
||||
|
||||
const fetchGraph = async ({
|
||||
esClient,
|
||||
logger,
|
||||
start,
|
||||
end,
|
||||
actorIds,
|
||||
eventIds,
|
||||
}: {
|
||||
esClient: IScopedClusterClient;
|
||||
logger: Logger;
|
||||
start: string | number;
|
||||
end: string | number;
|
||||
actorIds: string[];
|
||||
eventIds: string[];
|
||||
}): Promise<EsqlToRecords<GraphEdge>> => {
|
||||
const query = `from logs-*
|
||||
| WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL
|
||||
| EVAL isAlert = ${
|
||||
eventIds.length > 0
|
||||
? `event.id in (${eventIds.map((_id, idx) => `?al_id${idx}`).join(', ')})`
|
||||
: 'false'
|
||||
}
|
||||
| STATS badge = COUNT(*),
|
||||
ips = VALUES(related.ip),
|
||||
// hosts = VALUES(related.hosts),
|
||||
users = VALUES(related.user)
|
||||
by actorIds = actor.entity.id,
|
||||
action = event.action,
|
||||
targetIds = target.entity.id,
|
||||
eventOutcome = event.outcome,
|
||||
isAlert
|
||||
| LIMIT 1000`;
|
||||
|
||||
logger.trace(`Executing query [${query}]`);
|
||||
|
||||
return await esClient.asCurrentUser.helpers
|
||||
.esql({
|
||||
columnar: false,
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: start,
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
terms: {
|
||||
'event.id': eventIds,
|
||||
},
|
||||
},
|
||||
{
|
||||
terms: {
|
||||
'actor.entity.id': actorIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
query,
|
||||
// @ts-ignore - types are not up to date
|
||||
params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))],
|
||||
})
|
||||
.toRecords<GraphEdge>();
|
||||
};
|
||||
|
||||
const createNodes = (
|
||||
logger: Logger,
|
||||
records: GraphEdge[],
|
||||
context: Omit<ParseContext, 'edgesMap'>
|
||||
) => {
|
||||
const { nodesMap, edgeLabelsNodes } = context;
|
||||
|
||||
for (const record of records) {
|
||||
const { ips, hosts, users, actorIds, action, targetIds, isAlert, eventOutcome } = record;
|
||||
const actorIdsArray = castArray(actorIds);
|
||||
const targetIdsArray = castArray(targetIds);
|
||||
|
||||
logger.trace(
|
||||
`Parsing record [actorIds: ${actorIdsArray.join(
|
||||
', '
|
||||
)}, action: ${action}, targetIds: ${targetIdsArray.join(', ')}]`
|
||||
);
|
||||
|
||||
// Create entity nodes
|
||||
[...actorIdsArray, ...targetIdsArray].forEach((id) => {
|
||||
if (nodesMap[id] === undefined) {
|
||||
nodesMap[id] = {
|
||||
id,
|
||||
label: id,
|
||||
color: isAlert ? 'danger' : 'primary',
|
||||
...determineEntityNodeShape(id, ips ?? [], hosts ?? [], users ?? []),
|
||||
};
|
||||
|
||||
logger.trace(`Creating entity node [${id}]`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create label nodes
|
||||
for (const actorId of actorIdsArray) {
|
||||
for (const targetId of targetIdsArray) {
|
||||
const edgeId = `a(${actorId})-b(${targetId})`;
|
||||
|
||||
if (edgeLabelsNodes[edgeId] === undefined) {
|
||||
edgeLabelsNodes[edgeId] = [];
|
||||
}
|
||||
|
||||
const labelNode = {
|
||||
id: edgeId + `label(${action})outcome(${eventOutcome})`,
|
||||
label: action,
|
||||
source: actorId,
|
||||
target: targetId,
|
||||
color: isAlert ? 'danger' : eventOutcome === 'failed' ? 'warning' : 'primary',
|
||||
shape: 'label',
|
||||
} as LabelNodeDataModel;
|
||||
|
||||
logger.trace(`Creating label node [${labelNode.id}]`);
|
||||
|
||||
nodesMap[labelNode.id] = labelNode;
|
||||
edgeLabelsNodes[edgeId].push(labelNode.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const determineEntityNodeShape = (
|
||||
actorId: string,
|
||||
ips: string[],
|
||||
hosts: string[],
|
||||
users: string[]
|
||||
): {
|
||||
shape: EntityNodeDataModel['shape'];
|
||||
icon: string;
|
||||
} => {
|
||||
// If actor is a user return ellipse
|
||||
if (users.includes(actorId)) {
|
||||
return { shape: 'ellipse', icon: 'user' };
|
||||
}
|
||||
|
||||
// If actor is a host return hexagon
|
||||
if (hosts.includes(actorId)) {
|
||||
return { shape: 'hexagon', icon: 'storage' };
|
||||
}
|
||||
|
||||
// If actor is an IP return diamond
|
||||
if (ips.includes(actorId)) {
|
||||
return { shape: 'diamond', icon: 'globe' };
|
||||
}
|
||||
|
||||
return { shape: 'hexagon', icon: 'questionInCircle' };
|
||||
};
|
||||
|
||||
const sortNodes = (nodesMap: Record<string, NodeDataModel>) => {
|
||||
const groupNodes = [];
|
||||
const otherNodes = [];
|
||||
|
||||
for (const node of Object.values(nodesMap)) {
|
||||
if (node.shape === 'group') {
|
||||
groupNodes.push(node);
|
||||
} else {
|
||||
otherNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return [...groupNodes, ...otherNodes];
|
||||
};
|
||||
|
||||
const createEdgesAndGroups = (logger: Logger, context: ParseContext) => {
|
||||
const { edgeLabelsNodes, edgesMap, nodesMap } = context;
|
||||
|
||||
Object.entries(edgeLabelsNodes).forEach(([edgeId, edgeLabelsIds]) => {
|
||||
// When there's more than one edge label, create a group node
|
||||
if (edgeLabelsIds.length === 1) {
|
||||
const edgeLabelId = edgeLabelsIds[0];
|
||||
|
||||
connectEntitiesAndLabelNode(
|
||||
logger,
|
||||
edgesMap,
|
||||
nodesMap,
|
||||
(nodesMap[edgeLabelId] as LabelNodeDataModel).source,
|
||||
edgeLabelId,
|
||||
(nodesMap[edgeLabelId] as LabelNodeDataModel).target
|
||||
);
|
||||
} else {
|
||||
const groupNode: GroupNodeDataModel = {
|
||||
id: `grp(${edgeId})`,
|
||||
shape: 'group',
|
||||
};
|
||||
nodesMap[groupNode.id] = groupNode;
|
||||
|
||||
connectEntitiesAndLabelNode(
|
||||
logger,
|
||||
edgesMap,
|
||||
nodesMap,
|
||||
(nodesMap[edgeLabelsIds[0]] as LabelNodeDataModel).source,
|
||||
groupNode.id,
|
||||
(nodesMap[edgeLabelsIds[0]] as LabelNodeDataModel).target
|
||||
);
|
||||
|
||||
edgeLabelsIds.forEach((edgeLabelId) => {
|
||||
(nodesMap[edgeLabelId] as Writeable<LabelNodeDataModel>).parentId = groupNode.id;
|
||||
connectEntitiesAndLabelNode(
|
||||
logger,
|
||||
edgesMap,
|
||||
nodesMap,
|
||||
groupNode.id,
|
||||
edgeLabelId,
|
||||
groupNode.id
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const connectEntitiesAndLabelNode = (
|
||||
logger: Logger,
|
||||
edgesMap: Record<string, EdgeDataModel>,
|
||||
nodesMap: Record<string, NodeDataModel>,
|
||||
sourceNodeId: string,
|
||||
labelNodeId: string,
|
||||
targetNodeId: string
|
||||
) => {
|
||||
[
|
||||
connectNodes(nodesMap, sourceNodeId, labelNodeId),
|
||||
connectNodes(nodesMap, labelNodeId, targetNodeId),
|
||||
].forEach((edge) => {
|
||||
logger.trace(`Connecting nodes [${edge.source} -> ${edge.target}]`);
|
||||
edgesMap[edge.id] = edge;
|
||||
});
|
||||
};
|
||||
|
||||
const connectNodes = (
|
||||
nodesMap: Record<string, NodeDataModel>,
|
||||
sourceNodeId: string,
|
||||
targetNodeId: string
|
||||
) => {
|
||||
const sourceNode = nodesMap[sourceNodeId];
|
||||
const targetNode = nodesMap[targetNodeId];
|
||||
const color =
|
||||
sourceNode.shape !== 'group' && targetNode.shape !== 'label'
|
||||
? sourceNode.color
|
||||
: targetNode.shape !== 'group'
|
||||
? targetNode.color
|
||||
: 'primary';
|
||||
|
||||
return {
|
||||
id: `a(${sourceNodeId})-b(${targetNodeId})`,
|
||||
source: sourceNodeId,
|
||||
sourceShape: nodesMap[sourceNodeId].shape as NodeShape,
|
||||
target: targetNodeId,
|
||||
targetShape: nodesMap[targetNodeId].shape as NodeShape,
|
||||
color,
|
||||
} as EdgeDataModel;
|
||||
};
|
|
@ -27,6 +27,7 @@ import { defineGetDetectionEngineAlertsStatus } from './detection_engine/get_det
|
|||
import { defineBulkActionCspBenchmarkRulesRoute } from './benchmark_rules/bulk_action/bulk_action';
|
||||
import { defineGetCspBenchmarkRulesStatesRoute } from './benchmark_rules/get_states/get_states';
|
||||
import { setupCdrDataViews } from '../saved_objects/data_views';
|
||||
import { defineGraphRoute } from './graph/route';
|
||||
|
||||
/**
|
||||
* 1. Registers routes
|
||||
|
@ -50,6 +51,7 @@ export function setupRoutes({
|
|||
defineGetDetectionEngineAlertsStatus(router);
|
||||
defineBulkActionCspBenchmarkRulesRoute(router);
|
||||
defineGetCspBenchmarkRulesStatesRoute(router);
|
||||
defineGraphRoute(router);
|
||||
|
||||
core.http.registerOnPreRouting(async (request, response, toolkit) => {
|
||||
if (request.url.pathname.includes(CLOUD_SECURITY_INTERTAL_PREFIX_ROUTE_PATH)) {
|
||||
|
|
|
@ -39,6 +39,7 @@ import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface CspServerPluginSetup {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface CspServerPluginStart {}
|
||||
|
||||
|
@ -78,6 +79,8 @@ export interface CspApiRequestHandlerContext {
|
|||
agentService: AgentService;
|
||||
packagePolicyService: PackagePolicyClient;
|
||||
packageService: PackageService;
|
||||
spaces?: SpacesPluginStart;
|
||||
|
||||
isPluginInitialized(): boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@
|
|||
"@kbn/spaces-plugin",
|
||||
"@kbn/cloud-security-posture-common",
|
||||
"@kbn/cloud-security-posture",
|
||||
"@kbn/analytics"
|
||||
"@kbn/analytics",
|
||||
"@kbn/zod"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,499 @@
|
|||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"data_stream": "logs-gcp.audit-default",
|
||||
"id": "1",
|
||||
"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.CreateRole",
|
||||
"agent_id_status": "missing",
|
||||
"category": [
|
||||
"session",
|
||||
"network",
|
||||
"configuration"
|
||||
],
|
||||
"id": "kabcd1234efgh5678",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"data_stream": "logs-gcp.audit-default",
|
||||
"id": "2",
|
||||
"index": ".ds-logs-gcp.audit-default-2024.10.07-000001",
|
||||
"source": {
|
||||
"@timestamp": "2024-09-01T12:34:56.789Z",
|
||||
"actor": {
|
||||
"entity": {
|
||||
"id": "admin2@example.com"
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"user": {
|
||||
"email": "admin2@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"
|
||||
],
|
||||
"id": "failed-event",
|
||||
"ingested": "2024-10-07T17:47:35Z",
|
||||
"kind": "event",
|
||||
"outcome": "failed",
|
||||
"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": [
|
||||
"admin2@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": {
|
||||
"data_stream": "logs-gcp.audit-default",
|
||||
"id": "3",
|
||||
"index": ".ds-logs-gcp.audit-default-2024.10.07-000001",
|
||||
"source": {
|
||||
"@timestamp": "2024-09-01T12:34:56.789Z",
|
||||
"actor": {
|
||||
"entity": {
|
||||
"id": "admin3@example.com"
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"user": {
|
||||
"email": "admin3@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"
|
||||
],
|
||||
"id": "grouped-event1",
|
||||
"ingested": "2024-10-07T17:47:35Z",
|
||||
"kind": "event",
|
||||
"outcome": "failed",
|
||||
"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": [
|
||||
"admin3@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": {
|
||||
"data_stream": "logs-gcp.audit-default",
|
||||
"id": "4",
|
||||
"index": ".ds-logs-gcp.audit-default-2024.10.07-000001",
|
||||
"source": {
|
||||
"@timestamp": "2024-09-01T12:34:56.789Z",
|
||||
"actor": {
|
||||
"entity": {
|
||||
"id": "admin3@example.com"
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"user": {
|
||||
"email": "admin3@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"
|
||||
],
|
||||
"id": "grouped-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": [
|
||||
"admin3@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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,628 @@
|
|||
{
|
||||
"type": "data_stream",
|
||||
"value": {
|
||||
"data_stream": "logs-gcp.audit-default",
|
||||
"template": {
|
||||
"_meta": {
|
||||
"managed": true,
|
||||
"managed_by": "fleet",
|
||||
"package": {
|
||||
"name": "gcp"
|
||||
}
|
||||
},
|
||||
"data_stream": {
|
||||
"allow_custom_routing": false,
|
||||
"hidden": false
|
||||
},
|
||||
"ignore_missing_component_templates": [
|
||||
"logs-gcp.audit@custom"
|
||||
],
|
||||
"index_patterns": [
|
||||
"logs-gcp.audit-*"
|
||||
],
|
||||
"name": "logs-gcp.audit",
|
||||
"priority": 200,
|
||||
"template": {
|
||||
"mappings": {
|
||||
"_meta": {
|
||||
"managed": true,
|
||||
"managed_by": "fleet",
|
||||
"package": {
|
||||
"name": "gcp"
|
||||
}
|
||||
},
|
||||
"date_detection": false,
|
||||
"dynamic_templates": [
|
||||
{
|
||||
"ecs_message_match_only_text": {
|
||||
"mapping": {
|
||||
"type": "match_only_text"
|
||||
},
|
||||
"path_match": [
|
||||
"message",
|
||||
"*.message"
|
||||
],
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_non_indexed_keyword": {
|
||||
"mapping": {
|
||||
"doc_values": false,
|
||||
"index": false,
|
||||
"type": "keyword"
|
||||
},
|
||||
"path_match": [
|
||||
"*event.original"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_non_indexed_long": {
|
||||
"mapping": {
|
||||
"doc_values": false,
|
||||
"index": false,
|
||||
"type": "long"
|
||||
},
|
||||
"path_match": [
|
||||
"*.x509.public_key_exponent"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_ip": {
|
||||
"mapping": {
|
||||
"type": "ip"
|
||||
},
|
||||
"match_mapping_type": "string",
|
||||
"path_match": [
|
||||
"ip",
|
||||
"*.ip",
|
||||
"*_ip"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_wildcard": {
|
||||
"mapping": {
|
||||
"type": "wildcard"
|
||||
},
|
||||
"path_match": [
|
||||
"*.io.text",
|
||||
"*.message_id",
|
||||
"*registry.data.strings",
|
||||
"*url.path"
|
||||
],
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_path_match_wildcard_and_match_only_text": {
|
||||
"mapping": {
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "match_only_text"
|
||||
}
|
||||
},
|
||||
"type": "wildcard"
|
||||
},
|
||||
"path_match": [
|
||||
"*.body.content",
|
||||
"*url.full",
|
||||
"*url.original"
|
||||
],
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_match_wildcard_and_match_only_text": {
|
||||
"mapping": {
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "match_only_text"
|
||||
}
|
||||
},
|
||||
"type": "wildcard"
|
||||
},
|
||||
"match": [
|
||||
"*command_line",
|
||||
"*stack_trace"
|
||||
],
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_path_match_keyword_and_match_only_text": {
|
||||
"mapping": {
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "match_only_text"
|
||||
}
|
||||
},
|
||||
"type": "keyword"
|
||||
},
|
||||
"path_match": [
|
||||
"*.title",
|
||||
"*.executable",
|
||||
"*.name",
|
||||
"*.working_directory",
|
||||
"*.full_name",
|
||||
"*file.path",
|
||||
"*file.target_path",
|
||||
"*os.full",
|
||||
"*email.subject",
|
||||
"*vulnerability.description",
|
||||
"*user_agent.original"
|
||||
],
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_date": {
|
||||
"mapping": {
|
||||
"type": "date"
|
||||
},
|
||||
"path_match": [
|
||||
"*.timestamp",
|
||||
"*_timestamp",
|
||||
"*.not_after",
|
||||
"*.not_before",
|
||||
"*.accessed",
|
||||
"created",
|
||||
"*.created",
|
||||
"*.installed",
|
||||
"*.creation_date",
|
||||
"*.ctime",
|
||||
"*.mtime",
|
||||
"ingested",
|
||||
"*.ingested",
|
||||
"*.start",
|
||||
"*.end",
|
||||
"*.indicator.first_seen",
|
||||
"*.indicator.last_seen",
|
||||
"*.indicator.modified_at",
|
||||
"*threat.enrichments.matched.occurred"
|
||||
],
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_path_match_float": {
|
||||
"mapping": {
|
||||
"type": "float"
|
||||
},
|
||||
"path_match": [
|
||||
"*.score.*",
|
||||
"*_score*"
|
||||
],
|
||||
"path_unmatch": "*.version",
|
||||
"unmatch_mapping_type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_usage_double_scaled_float": {
|
||||
"mapping": {
|
||||
"scaling_factor": 1000,
|
||||
"type": "scaled_float"
|
||||
},
|
||||
"match_mapping_type": [
|
||||
"double",
|
||||
"long",
|
||||
"string"
|
||||
],
|
||||
"path_match": "*.usage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_geo_point": {
|
||||
"mapping": {
|
||||
"type": "geo_point"
|
||||
},
|
||||
"path_match": [
|
||||
"*.geo.location"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ecs_flattened": {
|
||||
"mapping": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"match_mapping_type": "object",
|
||||
"path_match": [
|
||||
"*structured_data",
|
||||
"*exports",
|
||||
"*imports"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"all_strings_to_keywords": {
|
||||
"mapping": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"match_mapping_type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"ignore_malformed": false,
|
||||
"type": "date"
|
||||
},
|
||||
"cloud": {
|
||||
"properties": {
|
||||
"image": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"data_stream": {
|
||||
"properties": {
|
||||
"dataset": {
|
||||
"type": "constant_keyword"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "constant_keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "constant_keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"data_stream.dataset": {
|
||||
"type": "constant_keyword"
|
||||
},
|
||||
"data_stream.namespace": {
|
||||
"type": "constant_keyword"
|
||||
},
|
||||
"data_stream.type": {
|
||||
"type": "constant_keyword",
|
||||
"value": "logs"
|
||||
},
|
||||
"event": {
|
||||
"properties": {
|
||||
"agent_id_status": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"dataset": {
|
||||
"type": "constant_keyword",
|
||||
"value": "gcp.audit"
|
||||
},
|
||||
"ingested": {
|
||||
"format": "strict_date_time_no_millis||strict_date_optional_time||epoch_millis",
|
||||
"ignore_malformed": false,
|
||||
"type": "date"
|
||||
},
|
||||
"module": {
|
||||
"type": "constant_keyword",
|
||||
"value": "gcp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gcp": {
|
||||
"properties": {
|
||||
"audit": {
|
||||
"properties": {
|
||||
"authentication_info": {
|
||||
"properties": {
|
||||
"authority_selector": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"principal_email": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"principal_subject": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"service_account_delegation_info": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"service_account_key_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"third_party_principal": {
|
||||
"type": "flattened"
|
||||
}
|
||||
}
|
||||
},
|
||||
"authorization_info": {
|
||||
"properties": {
|
||||
"granted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permission": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"resource": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"resource_attributes": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"service": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "nested"
|
||||
},
|
||||
"flattened": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"labels": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"logentry_operation": {
|
||||
"properties": {
|
||||
"first": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"last": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"producer": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"method_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"num_response_items": {
|
||||
"type": "long"
|
||||
},
|
||||
"policy_violation_info": {
|
||||
"properties": {
|
||||
"payload": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"resource_tags": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"resource_type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"violations": {
|
||||
"properties": {
|
||||
"checkedValue": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"constraint": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"errorMessage": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"policyType": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "nested"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"request_metadata": {
|
||||
"properties": {
|
||||
"caller_ip": {
|
||||
"type": "ip"
|
||||
},
|
||||
"caller_supplied_user_agent": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"raw": {
|
||||
"properties": {
|
||||
"caller_ip": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resource_location": {
|
||||
"properties": {
|
||||
"current_locations": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resource_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"response": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"service_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"status": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "long"
|
||||
},
|
||||
"details": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"message": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"destination": {
|
||||
"properties": {
|
||||
"instance": {
|
||||
"properties": {
|
||||
"project_id": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"region": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"zone": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vpc": {
|
||||
"properties": {
|
||||
"project_id": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"subnetwork_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"vpc_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"properties": {
|
||||
"instance": {
|
||||
"properties": {
|
||||
"project_id": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"region": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"zone": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vpc": {
|
||||
"properties": {
|
||||
"project_id": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"subnetwork_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"vpc_name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"properties": {
|
||||
"containerized": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"os": {
|
||||
"properties": {
|
||||
"build": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"codename": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"properties": {
|
||||
"offset": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"codec": "best_compression",
|
||||
"final_pipeline": ".fleet_final_pipeline-1",
|
||||
"mapping": {
|
||||
"ignore_malformed": "true",
|
||||
"total_fields": {
|
||||
"ignore_dynamic_beyond_limit": "true",
|
||||
"limit": "1000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
257
x-pack/test/cloud_security_posture_api/routes/graph.ts
Normal file
257
x-pack/test/cloud_security_posture_api/routes/graph.ts
Normal file
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
ELASTIC_HTTP_VERSION_HEADER,
|
||||
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
|
||||
} from '@kbn/core-http-common';
|
||||
import expect from '@kbn/expect';
|
||||
import type { Agent } from 'supertest';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
import { result } from '../utils';
|
||||
import { CspSecurityCommonProvider } from './helper/user_roles_utilites';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const cspSecurity = CspSecurityCommonProvider(providerContext);
|
||||
|
||||
const postGraph = (agent: Agent, body: any, auth?: { user: string; pass: string }) => {
|
||||
const req = agent
|
||||
.post('/internal/cloud_security_posture/graph')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.set('kbn-xsrf', 'xxxx');
|
||||
|
||||
if (auth) {
|
||||
req.auth(auth.user, auth.pass);
|
||||
}
|
||||
|
||||
return req.send(body);
|
||||
};
|
||||
|
||||
describe('POST /internal/cloud_security_posture/graph', () => {
|
||||
describe('Authorization', () => {
|
||||
it('should return 403 for user without read access', async () => {
|
||||
await postGraph(
|
||||
supertestWithoutAuth,
|
||||
{
|
||||
query: {
|
||||
actorIds: [],
|
||||
eventIds: [],
|
||||
start: 'now-1d/d',
|
||||
end: 'now/d',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: 'role_security_no_read_user',
|
||||
pass: cspSecurity.getPasswordForUser('role_security_no_read_user'),
|
||||
}
|
||||
).expect(result(403));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should return 400 when missing `actorIds` field', async () => {
|
||||
await postGraph(supertest, {
|
||||
query: {
|
||||
eventIds: [],
|
||||
start: 'now-1d/d',
|
||||
end: 'now/d',
|
||||
},
|
||||
}).expect(result(400));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Happy flows', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded(
|
||||
'x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an empty graph', async () => {
|
||||
const response = await postGraph(supertest, {
|
||||
query: {
|
||||
actorIds: [],
|
||||
eventIds: [],
|
||||
start: 'now-1d/d',
|
||||
end: 'now/d',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(0);
|
||||
expect(response.body).to.have.property('edges').length(0);
|
||||
});
|
||||
|
||||
it('should return a graph with nodes and edges by actor', async () => {
|
||||
const response = await postGraph(supertest, {
|
||||
query: {
|
||||
actorIds: ['admin@example.com'],
|
||||
eventIds: [],
|
||||
start: '2024-09-01T00:00:00Z',
|
||||
end: '2024-09-02T00:00:00Z',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(3);
|
||||
expect(response.body).to.have.property('edges').length(2);
|
||||
|
||||
response.body.nodes.forEach((node: any) => {
|
||||
expect(node).to.have.property('color');
|
||||
expect(node.color).equal(
|
||||
'primary',
|
||||
`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(
|
||||
'primary',
|
||||
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a graph with nodes and edges by alert', async () => {
|
||||
const response = await postGraph(supertest, {
|
||||
query: {
|
||||
actorIds: [],
|
||||
eventIds: ['kabcd1234efgh5678'],
|
||||
start: '2024-09-01T00:00:00Z',
|
||||
end: '2024-09-02T00:00:00Z',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(3);
|
||||
expect(response.body).to.have.property('edges').length(2);
|
||||
|
||||
response.body.nodes.forEach((node: any) => {
|
||||
expect(node).to.have.property('color');
|
||||
expect(node.color).equal(
|
||||
'danger',
|
||||
`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(
|
||||
'danger',
|
||||
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('color of alert of failed event should be danger', async () => {
|
||||
const response = await postGraph(supertest, {
|
||||
query: {
|
||||
actorIds: [],
|
||||
eventIds: ['failed-event'],
|
||||
start: '2024-09-01T00:00:00Z',
|
||||
end: '2024-09-02T00:00:00Z',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(3);
|
||||
expect(response.body).to.have.property('edges').length(2);
|
||||
|
||||
response.body.nodes.forEach((node: any) => {
|
||||
expect(node).to.have.property('color');
|
||||
expect(node.color).equal(
|
||||
'danger',
|
||||
`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(
|
||||
'danger',
|
||||
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('color of event of failed event should be warning', async () => {
|
||||
const response = await postGraph(supertest, {
|
||||
query: {
|
||||
actorIds: ['admin2@example.com'],
|
||||
eventIds: [],
|
||||
start: '2024-09-01T00:00:00Z',
|
||||
end: '2024-09-02T00:00:00Z',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(3);
|
||||
expect(response.body).to.have.property('edges').length(2);
|
||||
|
||||
response.body.nodes.forEach((node: any) => {
|
||||
expect(node).to.have.property('color');
|
||||
|
||||
expect(node.color).equal(
|
||||
node.shape === 'label' ? 'warning' : 'primary',
|
||||
`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(
|
||||
'warning',
|
||||
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('2 grouped of events, 1 failed, 1 success', async () => {
|
||||
const response = await postGraph(supertest, {
|
||||
query: {
|
||||
actorIds: ['admin3@example.com'],
|
||||
eventIds: [],
|
||||
start: '2024-09-01T00:00:00Z',
|
||||
end: '2024-09-02T00:00:00Z',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(5);
|
||||
expect(response.body).to.have.property('edges').length(6);
|
||||
|
||||
expect(response.body.nodes[0].shape).equal('group', 'Groups should be the first nodes');
|
||||
|
||||
response.body.nodes.forEach((node: any) => {
|
||||
if (node.shape !== 'group') {
|
||||
expect(node).to.have.property('color');
|
||||
expect(node.color).equal(
|
||||
node.shape === 'label' && node.id.includes('outcome(failed)') ? 'warning' : 'primary',
|
||||
`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(
|
||||
edge.id.includes('outcome(failed)') ? 'warning' : 'primary',
|
||||
`edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -24,6 +24,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./csp_benchmark_rules_get_states.ts'));
|
||||
loadTestFile(require.resolve('./benchmarks.ts'));
|
||||
loadTestFile(require.resolve('./status.ts'));
|
||||
loadTestFile(require.resolve('./graph.ts'));
|
||||
loadTestFile(require.resolve('./get_detection_engine_alerts_count_by_rule_tags'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { RetryService } from '@kbn/ftr-common-functional-services';
|
|||
import type { Agent } from 'supertest';
|
||||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import type { Client as EsClient } from '@elastic/elasticsearch';
|
||||
import type { CallbackHandler, Response } from 'superagent';
|
||||
import expect from '@kbn/expect';
|
||||
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||
|
||||
|
@ -35,6 +36,19 @@ export const waitForPluginInitialized = ({
|
|||
logger.debug('CSP plugin is initialized');
|
||||
});
|
||||
|
||||
export function result(status: number): CallbackHandler {
|
||||
return (err: any, res: Response) => {
|
||||
if ((res?.status || err.status) !== status) {
|
||||
const e = new Error(
|
||||
`Expected ${status} ,got ${res?.status || err.status} resp: ${
|
||||
res?.body ? JSON.stringify(res.body) : err.text
|
||||
}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class EsIndexDataProvider {
|
||||
private es: EsClient;
|
||||
private index: string;
|
||||
|
|
|
@ -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 expect from '@kbn/expect';
|
||||
import {
|
||||
ELASTIC_HTTP_VERSION_HEADER,
|
||||
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
|
||||
} from '@kbn/core-http-common';
|
||||
import { result } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils';
|
||||
import type { Agent } from 'supertest';
|
||||
import type { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const roleScopedSupertest = getService('roleScopedSupertest');
|
||||
let supertestViewer: Pick<Agent, 'post'>;
|
||||
|
||||
const postGraph = (agent: Pick<Agent, 'post'>, body: any) => {
|
||||
const req = agent
|
||||
.post('/internal/cloud_security_posture/graph')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.set('kbn-xsrf', 'xxxx');
|
||||
|
||||
return req.send(body);
|
||||
};
|
||||
|
||||
describe('POST /internal/cloud_security_posture/graph', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded(
|
||||
'x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit'
|
||||
);
|
||||
supertestViewer = await roleScopedSupertest.getSupertestWithRoleScope('viewer', {
|
||||
useCookieHeader: true, // to avoid generating API key and use Cookie header instead
|
||||
withInternalHeaders: true,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit');
|
||||
});
|
||||
|
||||
describe('Authorization', () => {
|
||||
it('should return an empty graph', async () => {
|
||||
const response = await postGraph(supertestViewer, {
|
||||
query: {
|
||||
actorIds: [],
|
||||
eventIds: [],
|
||||
start: 'now-1d/d',
|
||||
end: 'now/d',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(0);
|
||||
expect(response.body).to.have.property('edges').length(0);
|
||||
});
|
||||
|
||||
it('should return a graph with nodes and edges by actor', async () => {
|
||||
const response = await postGraph(supertestViewer, {
|
||||
query: {
|
||||
actorIds: ['admin@example.com'],
|
||||
eventIds: [],
|
||||
start: '2024-09-01T00:00:00Z',
|
||||
end: '2024-09-02T00:00:00Z',
|
||||
},
|
||||
}).expect(result(200));
|
||||
|
||||
expect(response.body).to.have.property('nodes').length(3);
|
||||
expect(response.body).to.have.property('edges').length(2);
|
||||
|
||||
response.body.nodes.forEach((node: any) => {
|
||||
expect(node).to.have.property('color');
|
||||
expect(node.color).equal(
|
||||
'primary',
|
||||
`node color mismatched [node: ${node.id}] [actual: ${node.color}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -10,14 +10,15 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
|
|||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('cloud_security_posture', function () {
|
||||
this.tags(['cloud_security_posture']);
|
||||
loadTestFile(require.resolve('./status/status_not_deployed_not_installed'));
|
||||
loadTestFile(require.resolve('./status/status_indexed'));
|
||||
loadTestFile(require.resolve('./status/status_indexing'));
|
||||
loadTestFile(require.resolve('./benchmark/v1'));
|
||||
loadTestFile(require.resolve('./benchmark/v2'));
|
||||
loadTestFile(require.resolve('./find_csp_benchmark_rule'));
|
||||
loadTestFile(require.resolve('./telemetry'));
|
||||
loadTestFile(require.resolve('./graph'));
|
||||
loadTestFile(require.resolve('./serverless_metering/cloud_security_metering'));
|
||||
loadTestFile(require.resolve('./status/status_indexed'));
|
||||
loadTestFile(require.resolve('./status/status_indexing'));
|
||||
loadTestFile(require.resolve('./status/status_not_deployed_not_installed'));
|
||||
loadTestFile(require.resolve('./telemetry'));
|
||||
|
||||
// TODO: migrate status_unprivileged tests from stateful, if it feasible in serverless with the new security model
|
||||
// loadTestFile(require.resolve('./status/status_unprivileged'));
|
||||
|
|
127
yarn.lock
127
yarn.lock
|
@ -346,10 +346,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
|
||||
integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
|
||||
|
||||
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0"
|
||||
integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==
|
||||
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
|
||||
version "7.24.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878"
|
||||
integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==
|
||||
|
||||
"@babel/helper-remap-async-to-generator@^7.24.7":
|
||||
version "7.24.7"
|
||||
|
@ -1012,12 +1012,12 @@
|
|||
"@babel/helper-plugin-utils" "^7.24.7"
|
||||
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
|
||||
|
||||
"@babel/plugin-transform-optional-chaining@^7.24.7":
|
||||
version "7.24.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454"
|
||||
integrity sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==
|
||||
"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8":
|
||||
version "7.24.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d"
|
||||
integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.24.7"
|
||||
"@babel/helper-plugin-utils" "^7.24.8"
|
||||
"@babel/helper-skip-transparent-expression-wrappers" "^7.24.7"
|
||||
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
|
||||
|
||||
|
@ -1563,6 +1563,18 @@
|
|||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@dagrejs/dagre@^1.1.4":
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.4.tgz#66f9c0e2b558308f2c268f60e2c28f22ee17e339"
|
||||
integrity sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==
|
||||
dependencies:
|
||||
"@dagrejs/graphlib" "2.2.4"
|
||||
|
||||
"@dagrejs/graphlib@2.2.4":
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.4.tgz#d77bfa9ff49e2307c0c6e6b8b26b5dd3c05816c4"
|
||||
integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==
|
||||
|
||||
"@dependents/detective-less@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@dependents/detective-less/-/detective-less-4.1.0.tgz#4a979ee7a6a79eb33602862d6a1263e30f98002e"
|
||||
|
@ -3631,6 +3643,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/cloud-security-posture-graph@link:x-pack/packages/kbn-cloud-security-posture/graph":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/cloud-security-posture-plugin@link:x-pack/plugins/cloud_security_posture":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -10338,6 +10354,20 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.1.tgz#570ea7f8b853461301804efa52bd790a640a26db"
|
||||
integrity sha512-u7LTCL7RnaavFSmob2rIAJLNwu50i6gFwY9cHFr80BrQURYQBRkJ+Yv47nA3Fm7FeRhdWTiVTeqvSeOuMAOzBQ==
|
||||
|
||||
"@types/d3-drag@^3.0.7":
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
|
||||
integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
|
||||
dependencies:
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/d3-interpolate@*":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
|
||||
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
|
||||
dependencies:
|
||||
"@types/d3-color" "*"
|
||||
|
||||
"@types/d3-interpolate@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
|
||||
|
@ -10362,6 +10392,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59"
|
||||
integrity sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ==
|
||||
|
||||
"@types/d3-selection@^3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
|
||||
integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
|
||||
|
||||
"@types/d3-shape@^2.1.0":
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3"
|
||||
|
@ -10381,6 +10416,21 @@
|
|||
dependencies:
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/d3-transition@^3.0.8":
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f"
|
||||
integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==
|
||||
dependencies:
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/d3-zoom@^3.0.8":
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
|
||||
integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
|
||||
dependencies:
|
||||
"@types/d3-interpolate" "*"
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/d3@^3.5.43":
|
||||
version "3.5.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.43.tgz#e9b4992817e0b6c5efaa7d6e5bb2cee4d73eab58"
|
||||
|
@ -12131,6 +12181,28 @@
|
|||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||
|
||||
"@xyflow/react@^12.3.0":
|
||||
version "12.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.3.0.tgz#611bf68e0580ff2eab0a5cdf4b997d923a8cae85"
|
||||
integrity sha512-dujEbjOn+5gMGg/wsojxtI7v2CfWm7ieRyiOHiZTPyw6p/VIdCoS3nLfSBP3TT+swoHSAXZ78iomHSKoUl4tMg==
|
||||
dependencies:
|
||||
"@xyflow/system" "0.0.42"
|
||||
classcat "^5.0.3"
|
||||
zustand "^4.4.0"
|
||||
|
||||
"@xyflow/system@0.0.42":
|
||||
version "0.0.42"
|
||||
resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.42.tgz#359b274fe072a28191ecfbc61bb7a3601de80e1e"
|
||||
integrity sha512-kWYj+Y0GOct0jKYTdyRMNOLPxGNbb2TYvPg2gTmJnZ31DOOMkL5uRBLX825DR2gOACDu+i5FHLxPJUPf/eGOJw==
|
||||
dependencies:
|
||||
"@types/d3-drag" "^3.0.7"
|
||||
"@types/d3-selection" "^3.0.10"
|
||||
"@types/d3-transition" "^3.0.8"
|
||||
"@types/d3-zoom" "^3.0.8"
|
||||
d3-drag "^3.0.0"
|
||||
d3-selection "^3.0.0"
|
||||
d3-zoom "^3.0.0"
|
||||
|
||||
"@yarnpkg/lockfile@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||
|
@ -14320,6 +14392,11 @@ class-utils@^0.3.5:
|
|||
lazy-cache "^2.0.2"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
classcat@^5.0.3:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77"
|
||||
integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==
|
||||
|
||||
classnames@2.2.6:
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
|
@ -15544,7 +15621,7 @@ d3-delaunay@^6.0.2:
|
|||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
|
||||
integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
|
||||
|
||||
"d3-drag@2 - 3":
|
||||
"d3-drag@2 - 3", d3-drag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
|
||||
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||
|
@ -15657,7 +15734,7 @@ d3-scale@^4.0.2:
|
|||
d3-time "2.1.1 - 3"
|
||||
d3-time-format "2 - 4"
|
||||
|
||||
d3-selection@3, d3-selection@^3.0.0:
|
||||
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||
|
@ -15709,7 +15786,7 @@ d3-shape@^3.2.0:
|
|||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||
|
||||
d3-transition@3, d3-transition@^3.0.1:
|
||||
"d3-transition@2 - 3", d3-transition@3, d3-transition@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
|
||||
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
|
||||
|
@ -15720,6 +15797,17 @@ d3-transition@3, d3-transition@^3.0.1:
|
|||
d3-interpolate "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
d3-zoom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
|
||||
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-drag "2 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-selection "2 - 3"
|
||||
d3-transition "2 - 3"
|
||||
|
||||
d3@3.5.17, d3@^3.5.6:
|
||||
version "3.5.17"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
|
||||
|
@ -31449,10 +31537,10 @@ use-sidecar@^1.1.2:
|
|||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
use-sync-external-store@1.2.2, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
||||
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
||||
|
||||
use@^2.0.0:
|
||||
version "2.0.2"
|
||||
|
@ -33066,6 +33154,13 @@ zod@3.23.8, zod@^3.22.3, zod@^3.22.4, zod@^3.23.8:
|
|||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
||||
zustand@^4.4.0:
|
||||
version "4.5.5"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1"
|
||||
integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.2"
|
||||
|
||||
zwitch@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue