@cyvest/cyvest-vis 4.4.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,7 +85,7 @@ import { ObservablesGraph } from "@cyvest/cyvest-vis";
85
85
 
86
86
  #### `InvestigationGraph`
87
87
 
88
- Hierarchical graph showing root → containers → checks structure.
88
+ Hierarchical graph showing root → tags → checks structure.
89
89
 
90
90
  ```tsx
91
91
  import { InvestigationGraph } from "@cyvest/cyvest-vis";
@@ -95,7 +95,7 @@ import { InvestigationGraph } from "@cyvest/cyvest-vis";
95
95
  height={600}
96
96
  width="100%"
97
97
  onNodeClick={(nodeId, nodeType) => {
98
- // nodeType: "root" | "check" | "container"
98
+ // nodeType: "root" | "check" | "tag"
99
99
  }}
100
100
  />
101
101
  ```
@@ -116,7 +116,7 @@ const Icon = getObservableIcon("ipv4-addr"); // Returns GlobeIcon
116
116
  <MailIcon size={16} color="#ef4444" />
117
117
  ```
118
118
 
119
- Available icons: `GlobeIcon`, `DomainIcon`, `LinkIcon`, `MailIcon`, `EnvelopeIcon`, `FileIcon`, `HashIcon`, `UserIcon`, `IdCardIcon`, `GearIcon`, `AppIcon`, `RegistryIcon`, `ThreatActorIcon`, `BugIcon`, `SwordIcon`, `TargetIcon`, `AlertIcon`, `FlaskIcon`, `CertificateIcon`, `WifiIcon`, `WorldIcon`, `QuestionIcon`, `CheckIcon`, `BoxIcon`, `CrosshairIcon`
119
+ Available icons: `GlobeIcon`, `DomainIcon`, `LinkIcon`, `MailIcon`, `EnvelopeIcon`, `FileIcon`, `HashIcon`, `UserIcon`, `IdCardIcon`, `GearIcon`, `AppIcon`, `RegistryIcon`, `ThreatActorIcon`, `BugIcon`, `SwordIcon`, `TargetIcon`, `AlertIcon`, `FlaskIcon`, `CertificateIcon`, `WifiIcon`, `WorldIcon`, `QuestionIcon`, `CheckIcon`, `TagIcon`, `CrosshairIcon`
120
120
 
121
121
  ## Types
122
122
 
package/dist/index.cjs CHANGED
@@ -627,7 +627,7 @@ var CheckIcon = ({
627
627
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 6 9 17l-5-5" })
628
628
  }
629
629
  );
630
- var BoxIcon = ({
630
+ var TagIcon = ({
631
631
  size = defaultSize,
632
632
  color = defaultColor,
633
633
  className
@@ -716,7 +716,7 @@ var OBSERVABLE_ICON_MAP = {
716
716
  var INVESTIGATION_ICON_MAP = {
717
717
  root: CrosshairIcon,
718
718
  check: CheckIcon,
719
- container: BoxIcon
719
+ tag: TagIcon
720
720
  };
721
721
  function getObservableIcon(observableType) {
722
722
  const normalized = observableType.toLowerCase().trim();
@@ -1485,6 +1485,7 @@ var ObservablesGraph = (props) => {
1485
1485
  var import_react12 = __toESM(require("react"), 1);
1486
1486
  var import_react13 = require("@xyflow/react");
1487
1487
  var import_style2 = require("@xyflow/react/dist/style.css");
1488
+ var import_cyvest_js3 = require("@cyvest/cyvest-js");
1488
1489
 
1489
1490
  // src/components/InvestigationNode.tsx
1490
1491
  var import_react9 = require("react");
@@ -1513,7 +1514,7 @@ var NODE_CONFIG = {
1513
1514
  alignCenter: false
1514
1515
  // Left-aligned
1515
1516
  },
1516
- container: {
1517
+ tag: {
1517
1518
  minWidth: 120,
1518
1519
  padding: "8px 14px",
1519
1520
  borderRadius: 16,
@@ -1671,15 +1672,8 @@ var defaultEdgeOptions2 = {
1671
1672
  color: "#94a3b8"
1672
1673
  }
1673
1674
  };
1674
- function flattenContainers(containers) {
1675
- const result = [];
1676
- for (const container of Object.values(containers)) {
1677
- result.push(container);
1678
- if (container.sub_containers) {
1679
- result.push(...flattenContainers(container.sub_containers));
1680
- }
1681
- }
1682
- return result;
1675
+ function getAllTags(tags) {
1676
+ return Object.values(tags);
1683
1677
  }
1684
1678
  function createInvestigationGraph(investigation) {
1685
1679
  const nodes = [];
@@ -1707,23 +1701,20 @@ function createInvestigationGraph(investigation) {
1707
1701
  selectable: true,
1708
1702
  draggable: true
1709
1703
  });
1710
- const allContainers = flattenContainers(investigation.containers);
1711
- const checksInContainers = /* @__PURE__ */ new Set();
1712
- for (const container of allContainers) {
1713
- for (const checkKey of container.checks) {
1714
- checksInContainers.add(checkKey);
1704
+ const allTags = getAllTags(investigation.tags);
1705
+ const checksInTags = /* @__PURE__ */ new Set();
1706
+ for (const tag of allTags) {
1707
+ for (const checkKey of tag.checks) {
1708
+ checksInTags.add(checkKey);
1715
1709
  }
1716
1710
  }
1717
- const allChecks = [];
1718
- for (const checksForKey of Object.values(investigation.checks)) {
1719
- allChecks.push(...checksForKey);
1720
- }
1711
+ const allChecks = Object.values(investigation.checks);
1721
1712
  const seenCheckIds = /* @__PURE__ */ new Set();
1722
1713
  for (const check of allChecks) {
1723
1714
  if (seenCheckIds.has(check.key)) continue;
1724
1715
  seenCheckIds.add(check.key);
1725
1716
  const checkNodeData = {
1726
- label: truncateLabel(check.check_id, 20),
1717
+ label: truncateLabel(check.check_name, 20),
1727
1718
  nodeType: "check",
1728
1719
  level: check.level,
1729
1720
  score: check.score,
@@ -1737,7 +1728,7 @@ function createInvestigationGraph(investigation) {
1737
1728
  selectable: true,
1738
1729
  draggable: true
1739
1730
  });
1740
- if (!checksInContainers.has(check.key)) {
1731
+ if (!checksInTags.has(check.key)) {
1741
1732
  edges.push({
1742
1733
  id: `edge-root-${check.key}`,
1743
1734
  source: rootKey,
@@ -1747,37 +1738,63 @@ function createInvestigationGraph(investigation) {
1747
1738
  });
1748
1739
  }
1749
1740
  }
1750
- for (const container of allContainers) {
1751
- const containerNodeData = {
1752
- label: truncateLabel(
1753
- container.path.split("/").pop() ?? container.path,
1754
- 20
1755
- ),
1756
- nodeType: "container",
1757
- level: container.aggregated_level,
1758
- score: container.aggregated_score,
1759
- path: container.path
1741
+ const tagByName = /* @__PURE__ */ new Map();
1742
+ for (const tag of allTags) {
1743
+ tagByName.set(tag.name, tag);
1744
+ }
1745
+ const allTagNames = /* @__PURE__ */ new Set();
1746
+ for (const tag of allTags) {
1747
+ allTagNames.add(tag.name);
1748
+ for (const ancestor of (0, import_cyvest_js3.getTagAncestors)(tag.name)) {
1749
+ allTagNames.add(ancestor);
1750
+ }
1751
+ }
1752
+ for (const tagName of allTagNames) {
1753
+ const realTag = tagByName.get(tagName);
1754
+ const tagNodeData = {
1755
+ label: truncateLabel(tagName.split(":").pop() ?? tagName, 20),
1756
+ nodeType: "tag",
1757
+ level: realTag?.direct_level ?? "INFO",
1758
+ score: realTag?.direct_score ?? 0,
1759
+ name: tagName
1760
1760
  };
1761
1761
  nodes.push({
1762
- id: `container-${container.key}`,
1762
+ id: `tag-${tagName}`,
1763
1763
  type: "investigation",
1764
1764
  position: { x: 0, y: 0 },
1765
- data: containerNodeData,
1765
+ data: tagNodeData,
1766
1766
  selectable: true,
1767
1767
  draggable: true
1768
1768
  });
1769
- edges.push({
1770
- id: `edge-root-container-${container.key}`,
1771
- source: rootKey,
1772
- target: `container-${container.key}`,
1773
- type: "smoothstep",
1774
- animated: false
1775
- });
1776
- for (const checkKey of container.checks) {
1769
+ }
1770
+ for (const tagName of allTagNames) {
1771
+ const nodeId = `tag-${tagName}`;
1772
+ const parts = tagName.split(":");
1773
+ if (parts.length === 1) {
1774
+ edges.push({
1775
+ id: `edge-root-tag-${tagName}`,
1776
+ source: rootKey,
1777
+ target: nodeId,
1778
+ type: "smoothstep",
1779
+ animated: false
1780
+ });
1781
+ } else {
1782
+ const parentName = parts.slice(0, -1).join(":");
1783
+ edges.push({
1784
+ id: `edge-tag-${parentName}-${tagName}`,
1785
+ source: `tag-${parentName}`,
1786
+ target: nodeId,
1787
+ type: "smoothstep",
1788
+ animated: false
1789
+ });
1790
+ }
1791
+ }
1792
+ for (const tag of allTags) {
1793
+ for (const checkKey of tag.checks) {
1777
1794
  if (seenCheckIds.has(checkKey)) {
1778
1795
  edges.push({
1779
- id: `edge-container-check-${container.key}-${checkKey}`,
1780
- source: `container-${container.key}`,
1796
+ id: `edge-tag-check-${tag.name}-${checkKey}`,
1797
+ source: `tag-${tag.name}`,
1781
1798
  target: `check-${checkKey}`,
1782
1799
  type: "smoothstep",
1783
1800
  animated: false
package/dist/index.d.cts CHANGED
@@ -42,14 +42,14 @@ interface ObservableEdgeData extends Record<string, unknown> {
42
42
  /**
43
43
  * Node types for the investigation graph view.
44
44
  */
45
- type InvestigationNodeType = "root" | "check" | "container";
45
+ type InvestigationNodeType = "root" | "check" | "tag";
46
46
  /**
47
47
  * Data attached to investigation graph nodes.
48
48
  */
49
49
  interface InvestigationNodeData extends Record<string, unknown> {
50
50
  /** Display label */
51
51
  label: string;
52
- /** Node type (root, check, or container) */
52
+ /** Node type (root, check, or tag) */
53
53
  nodeType: InvestigationNodeType;
54
54
  /** Security level */
55
55
  level: Level;
@@ -57,8 +57,8 @@ interface InvestigationNodeData extends Record<string, unknown> {
57
57
  score: number;
58
58
  /** Description (for checks) */
59
59
  description?: string;
60
- /** Path (for containers) */
61
- path?: string;
60
+ /** Name (for tags) */
61
+ name?: string;
62
62
  }
63
63
  /**
64
64
  * Configuration options for d3-force layout.
@@ -159,7 +159,7 @@ declare const ObservablesGraph: React.FC<ObservablesGraphProps>;
159
159
 
160
160
  /**
161
161
  * InvestigationGraph component - displays investigation structure with Dagre layout.
162
- * Shows root observable, checks, and containers in a hierarchical view.
162
+ * Shows root observable, checks, and tags in a hierarchical view.
163
163
  */
164
164
 
165
165
  /**
package/dist/index.d.ts CHANGED
@@ -42,14 +42,14 @@ interface ObservableEdgeData extends Record<string, unknown> {
42
42
  /**
43
43
  * Node types for the investigation graph view.
44
44
  */
45
- type InvestigationNodeType = "root" | "check" | "container";
45
+ type InvestigationNodeType = "root" | "check" | "tag";
46
46
  /**
47
47
  * Data attached to investigation graph nodes.
48
48
  */
49
49
  interface InvestigationNodeData extends Record<string, unknown> {
50
50
  /** Display label */
51
51
  label: string;
52
- /** Node type (root, check, or container) */
52
+ /** Node type (root, check, or tag) */
53
53
  nodeType: InvestigationNodeType;
54
54
  /** Security level */
55
55
  level: Level;
@@ -57,8 +57,8 @@ interface InvestigationNodeData extends Record<string, unknown> {
57
57
  score: number;
58
58
  /** Description (for checks) */
59
59
  description?: string;
60
- /** Path (for containers) */
61
- path?: string;
60
+ /** Name (for tags) */
61
+ name?: string;
62
62
  }
63
63
  /**
64
64
  * Configuration options for d3-force layout.
@@ -159,7 +159,7 @@ declare const ObservablesGraph: React.FC<ObservablesGraphProps>;
159
159
 
160
160
  /**
161
161
  * InvestigationGraph component - displays investigation structure with Dagre layout.
162
- * Shows root observable, checks, and containers in a hierarchical view.
162
+ * Shows root observable, checks, and tags in a hierarchical view.
163
163
  */
164
164
 
165
165
  /**
package/dist/index.js CHANGED
@@ -595,7 +595,7 @@ var CheckIcon = ({
595
595
  children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
596
596
  }
597
597
  );
598
- var BoxIcon = ({
598
+ var TagIcon = ({
599
599
  size = defaultSize,
600
600
  color = defaultColor,
601
601
  className
@@ -684,7 +684,7 @@ var OBSERVABLE_ICON_MAP = {
684
684
  var INVESTIGATION_ICON_MAP = {
685
685
  root: CrosshairIcon,
686
686
  check: CheckIcon,
687
- container: BoxIcon
687
+ tag: TagIcon
688
688
  };
689
689
  function getObservableIcon(observableType) {
690
690
  const normalized = observableType.toLowerCase().trim();
@@ -1474,6 +1474,7 @@ import {
1474
1474
  MarkerType
1475
1475
  } from "@xyflow/react";
1476
1476
  import "@xyflow/react/dist/style.css";
1477
+ import { getTagAncestors } from "@cyvest/cyvest-js";
1477
1478
 
1478
1479
  // src/components/InvestigationNode.tsx
1479
1480
  import { memo as memo3, useMemo as useMemo5 } from "react";
@@ -1502,7 +1503,7 @@ var NODE_CONFIG = {
1502
1503
  alignCenter: false
1503
1504
  // Left-aligned
1504
1505
  },
1505
- container: {
1506
+ tag: {
1506
1507
  minWidth: 120,
1507
1508
  padding: "8px 14px",
1508
1509
  borderRadius: 16,
@@ -1660,15 +1661,8 @@ var defaultEdgeOptions2 = {
1660
1661
  color: "#94a3b8"
1661
1662
  }
1662
1663
  };
1663
- function flattenContainers(containers) {
1664
- const result = [];
1665
- for (const container of Object.values(containers)) {
1666
- result.push(container);
1667
- if (container.sub_containers) {
1668
- result.push(...flattenContainers(container.sub_containers));
1669
- }
1670
- }
1671
- return result;
1664
+ function getAllTags(tags) {
1665
+ return Object.values(tags);
1672
1666
  }
1673
1667
  function createInvestigationGraph(investigation) {
1674
1668
  const nodes = [];
@@ -1696,23 +1690,20 @@ function createInvestigationGraph(investigation) {
1696
1690
  selectable: true,
1697
1691
  draggable: true
1698
1692
  });
1699
- const allContainers = flattenContainers(investigation.containers);
1700
- const checksInContainers = /* @__PURE__ */ new Set();
1701
- for (const container of allContainers) {
1702
- for (const checkKey of container.checks) {
1703
- checksInContainers.add(checkKey);
1693
+ const allTags = getAllTags(investigation.tags);
1694
+ const checksInTags = /* @__PURE__ */ new Set();
1695
+ for (const tag of allTags) {
1696
+ for (const checkKey of tag.checks) {
1697
+ checksInTags.add(checkKey);
1704
1698
  }
1705
1699
  }
1706
- const allChecks = [];
1707
- for (const checksForKey of Object.values(investigation.checks)) {
1708
- allChecks.push(...checksForKey);
1709
- }
1700
+ const allChecks = Object.values(investigation.checks);
1710
1701
  const seenCheckIds = /* @__PURE__ */ new Set();
1711
1702
  for (const check of allChecks) {
1712
1703
  if (seenCheckIds.has(check.key)) continue;
1713
1704
  seenCheckIds.add(check.key);
1714
1705
  const checkNodeData = {
1715
- label: truncateLabel(check.check_id, 20),
1706
+ label: truncateLabel(check.check_name, 20),
1716
1707
  nodeType: "check",
1717
1708
  level: check.level,
1718
1709
  score: check.score,
@@ -1726,7 +1717,7 @@ function createInvestigationGraph(investigation) {
1726
1717
  selectable: true,
1727
1718
  draggable: true
1728
1719
  });
1729
- if (!checksInContainers.has(check.key)) {
1720
+ if (!checksInTags.has(check.key)) {
1730
1721
  edges.push({
1731
1722
  id: `edge-root-${check.key}`,
1732
1723
  source: rootKey,
@@ -1736,37 +1727,63 @@ function createInvestigationGraph(investigation) {
1736
1727
  });
1737
1728
  }
1738
1729
  }
1739
- for (const container of allContainers) {
1740
- const containerNodeData = {
1741
- label: truncateLabel(
1742
- container.path.split("/").pop() ?? container.path,
1743
- 20
1744
- ),
1745
- nodeType: "container",
1746
- level: container.aggregated_level,
1747
- score: container.aggregated_score,
1748
- path: container.path
1730
+ const tagByName = /* @__PURE__ */ new Map();
1731
+ for (const tag of allTags) {
1732
+ tagByName.set(tag.name, tag);
1733
+ }
1734
+ const allTagNames = /* @__PURE__ */ new Set();
1735
+ for (const tag of allTags) {
1736
+ allTagNames.add(tag.name);
1737
+ for (const ancestor of getTagAncestors(tag.name)) {
1738
+ allTagNames.add(ancestor);
1739
+ }
1740
+ }
1741
+ for (const tagName of allTagNames) {
1742
+ const realTag = tagByName.get(tagName);
1743
+ const tagNodeData = {
1744
+ label: truncateLabel(tagName.split(":").pop() ?? tagName, 20),
1745
+ nodeType: "tag",
1746
+ level: realTag?.direct_level ?? "INFO",
1747
+ score: realTag?.direct_score ?? 0,
1748
+ name: tagName
1749
1749
  };
1750
1750
  nodes.push({
1751
- id: `container-${container.key}`,
1751
+ id: `tag-${tagName}`,
1752
1752
  type: "investigation",
1753
1753
  position: { x: 0, y: 0 },
1754
- data: containerNodeData,
1754
+ data: tagNodeData,
1755
1755
  selectable: true,
1756
1756
  draggable: true
1757
1757
  });
1758
- edges.push({
1759
- id: `edge-root-container-${container.key}`,
1760
- source: rootKey,
1761
- target: `container-${container.key}`,
1762
- type: "smoothstep",
1763
- animated: false
1764
- });
1765
- for (const checkKey of container.checks) {
1758
+ }
1759
+ for (const tagName of allTagNames) {
1760
+ const nodeId = `tag-${tagName}`;
1761
+ const parts = tagName.split(":");
1762
+ if (parts.length === 1) {
1763
+ edges.push({
1764
+ id: `edge-root-tag-${tagName}`,
1765
+ source: rootKey,
1766
+ target: nodeId,
1767
+ type: "smoothstep",
1768
+ animated: false
1769
+ });
1770
+ } else {
1771
+ const parentName = parts.slice(0, -1).join(":");
1772
+ edges.push({
1773
+ id: `edge-tag-${parentName}-${tagName}`,
1774
+ source: `tag-${parentName}`,
1775
+ target: nodeId,
1776
+ type: "smoothstep",
1777
+ animated: false
1778
+ });
1779
+ }
1780
+ }
1781
+ for (const tag of allTags) {
1782
+ for (const checkKey of tag.checks) {
1766
1783
  if (seenCheckIds.has(checkKey)) {
1767
1784
  edges.push({
1768
- id: `edge-container-check-${container.key}-${checkKey}`,
1769
- source: `container-${container.key}`,
1785
+ id: `edge-tag-check-${tag.name}-${checkKey}`,
1786
+ source: `tag-${tag.name}`,
1770
1787
  target: `check-${checkKey}`,
1771
1788
  type: "smoothstep",
1772
1789
  animated: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyvest/cyvest-vis",
3
- "version": "4.4.1",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "@dagrejs/dagre": "^1.1.8",
27
27
  "@xyflow/react": "^12.10.0",
28
28
  "d3-force": "^3.0.0",
29
- "@cyvest/cyvest-js": "4.4.1"
29
+ "@cyvest/cyvest-js": "5.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/d3-force": "^3.0.10",
@@ -594,9 +594,9 @@ export const CheckIcon: React.FC<IconProps> = ({
594
594
  );
595
595
 
596
596
  /**
597
- * Box icon for containers
597
+ * Tag icon for tags
598
598
  */
599
- export const BoxIcon: React.FC<IconProps> = ({
599
+ export const TagIcon: React.FC<IconProps> = ({
600
600
  size = defaultSize,
601
601
  color = defaultColor,
602
602
  className,
@@ -706,7 +706,7 @@ export const INVESTIGATION_ICON_MAP: Record<
706
706
  > = {
707
707
  root: CrosshairIcon,
708
708
  check: CheckIcon,
709
- container: BoxIcon,
709
+ tag: TagIcon,
710
710
  };
711
711
 
712
712
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * InvestigationGraph component - displays investigation structure with Dagre layout.
3
- * Shows root observable, checks, and containers in a hierarchical view.
3
+ * Shows root observable, checks, and tags in a hierarchical view.
4
4
  */
5
5
 
6
6
  import React, { useMemo, useCallback } from "react";
@@ -19,7 +19,8 @@ import {
19
19
  } from "@xyflow/react";
20
20
  import "@xyflow/react/dist/style.css";
21
21
 
22
- import type { CyvestInvestigation, Check, Container } from "@cyvest/cyvest-js";
22
+ import type { CyvestInvestigation, Check, Tag } from "@cyvest/cyvest-js";
23
+ import { getTagAncestors } from "@cyvest/cyvest-js";
23
24
 
24
25
  import type {
25
26
  InvestigationGraphProps,
@@ -55,21 +56,10 @@ const defaultEdgeOptions = {
55
56
  };
56
57
 
57
58
  /**
58
- * Flatten containers recursively to get all container keys.
59
+ * Get all tags as an array.
59
60
  */
60
- function flattenContainers(
61
- containers: Record<string, Container>
62
- ): Container[] {
63
- const result: Container[] = [];
64
-
65
- for (const container of Object.values(containers)) {
66
- result.push(container);
67
- if (container.sub_containers) {
68
- result.push(...flattenContainers(container.sub_containers));
69
- }
70
- }
71
-
72
- return result;
61
+ function getAllTags(tags: Record<string, Tag>): Tag[] {
62
+ return Object.values(tags);
73
63
  }
74
64
 
75
65
  /**
@@ -115,31 +105,27 @@ function createInvestigationGraph(
115
105
  draggable: true,
116
106
  });
117
107
 
118
- // Collect all check keys that belong to containers
108
+ // Collect all check keys that belong to tags
119
109
  // These checks should NOT have a direct link to the root node
120
- const allContainers = flattenContainers(investigation.containers);
121
- const checksInContainers = new Set<string>();
122
- for (const container of allContainers) {
123
- for (const checkKey of container.checks) {
124
- checksInContainers.add(checkKey);
110
+ const allTags = getAllTags(investigation.tags);
111
+ const checksInTags = new Set<string>();
112
+ for (const tag of allTags) {
113
+ for (const checkKey of tag.checks) {
114
+ checksInTags.add(checkKey);
125
115
  }
126
116
  }
127
117
 
128
118
  // Add check nodes
129
- // Group checks by scope for better organization
130
- const allChecks: Check[] = [];
131
- for (const checksForKey of Object.values(investigation.checks)) {
132
- allChecks.push(...checksForKey);
133
- }
119
+ const allChecks = Object.values(investigation.checks);
134
120
 
135
- // Create unique check nodes (by check_id to avoid duplicates)
121
+ // Create unique check nodes (by key to avoid duplicates)
136
122
  const seenCheckIds = new Set<string>();
137
123
  for (const check of allChecks) {
138
124
  if (seenCheckIds.has(check.key)) continue;
139
125
  seenCheckIds.add(check.key);
140
126
 
141
127
  const checkNodeData: InvestigationNodeData = {
142
- label: truncateLabel(check.check_id, 20),
128
+ label: truncateLabel(check.check_name, 20),
143
129
  nodeType: "check",
144
130
  level: check.level,
145
131
  score: check.score,
@@ -155,9 +141,9 @@ function createInvestigationGraph(
155
141
  draggable: true,
156
142
  });
157
143
 
158
- // Only create edge from root to check if check is NOT in a container
159
- // Checks in containers will be linked through their container instead
160
- if (!checksInContainers.has(check.key)) {
144
+ // Only create edge from root to check if check is NOT in a tag
145
+ // Checks in tags will be linked through their tag instead
146
+ if (!checksInTags.has(check.key)) {
161
147
  edges.push({
162
148
  id: `edge-root-${check.key}`,
163
149
  source: rootKey,
@@ -168,43 +154,79 @@ function createInvestigationGraph(
168
154
  }
169
155
  }
170
156
 
171
- // Add container nodes
172
- for (const container of allContainers) {
173
- const containerNodeData: InvestigationNodeData = {
174
- label: truncateLabel(
175
- container.path.split("/").pop() ?? container.path,
176
- 20
177
- ),
178
- nodeType: "container",
179
- level: container.aggregated_level,
180
- score: container.aggregated_score,
181
- path: container.path,
157
+ // Build hierarchical tag structure
158
+ // First, collect all tag names and find/create ancestor tags
159
+ const tagByName = new Map<string, Tag>();
160
+ for (const tag of allTags) {
161
+ tagByName.set(tag.name, tag);
162
+ }
163
+
164
+ // Collect all unique tag names including synthetic ancestors
165
+ const allTagNames = new Set<string>();
166
+ for (const tag of allTags) {
167
+ allTagNames.add(tag.name);
168
+ // Add ancestors (they may not exist as actual tags)
169
+ for (const ancestor of getTagAncestors(tag.name)) {
170
+ allTagNames.add(ancestor);
171
+ }
172
+ }
173
+
174
+ // Create tag nodes (real and synthetic)
175
+ for (const tagName of allTagNames) {
176
+ const realTag = tagByName.get(tagName);
177
+
178
+ const tagNodeData: InvestigationNodeData = {
179
+ label: truncateLabel(tagName.split(":").pop() ?? tagName, 20),
180
+ nodeType: "tag",
181
+ level: realTag?.direct_level ?? "INFO",
182
+ score: realTag?.direct_score ?? 0,
183
+ name: tagName,
182
184
  };
183
185
 
184
186
  nodes.push({
185
- id: `container-${container.key}`,
187
+ id: `tag-${tagName}`,
186
188
  type: "investigation",
187
189
  position: { x: 0, y: 0 },
188
- data: containerNodeData,
190
+ data: tagNodeData,
189
191
  selectable: true,
190
192
  draggable: true,
191
193
  });
194
+ }
192
195
 
193
- // Edge from root to container
194
- edges.push({
195
- id: `edge-root-container-${container.key}`,
196
- source: rootKey,
197
- target: `container-${container.key}`,
198
- type: "smoothstep",
199
- animated: false,
200
- });
196
+ // Create edges based on tag hierarchy
197
+ for (const tagName of allTagNames) {
198
+ const nodeId = `tag-${tagName}`;
199
+ const parts = tagName.split(":");
200
+
201
+ if (parts.length === 1) {
202
+ // Top-level tag, connect to root
203
+ edges.push({
204
+ id: `edge-root-tag-${tagName}`,
205
+ source: rootKey,
206
+ target: nodeId,
207
+ type: "smoothstep",
208
+ animated: false,
209
+ });
210
+ } else {
211
+ // Has a parent tag, connect to parent
212
+ const parentName = parts.slice(0, -1).join(":");
213
+ edges.push({
214
+ id: `edge-tag-${parentName}-${tagName}`,
215
+ source: `tag-${parentName}`,
216
+ target: nodeId,
217
+ type: "smoothstep",
218
+ animated: false,
219
+ });
220
+ }
221
+ }
201
222
 
202
- // Edges from container to its checks
203
- for (const checkKey of container.checks) {
223
+ // Create edges from tags to their checks (only for real tags with checks)
224
+ for (const tag of allTags) {
225
+ for (const checkKey of tag.checks) {
204
226
  if (seenCheckIds.has(checkKey)) {
205
227
  edges.push({
206
- id: `edge-container-check-${container.key}-${checkKey}`,
207
- source: `container-${container.key}`,
228
+ id: `edge-tag-check-${tag.name}-${checkKey}`,
229
+ source: `tag-${tag.name}`,
208
230
  target: `check-${checkKey}`,
209
231
  type: "smoothstep",
210
232
  animated: false,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Custom node component for the Investigation Graph (Dagre layout).
3
- * Professional design with SVG icons for root, check, and container nodes.
3
+ * Professional design with SVG icons for root, check, and tag nodes.
4
4
  */
5
5
 
6
6
  import React, { memo, useMemo } from "react";
@@ -33,7 +33,7 @@ const NODE_CONFIG = {
33
33
  showIcon: false, // No icon for checks
34
34
  alignCenter: false, // Left-aligned
35
35
  },
36
- container: {
36
+ tag: {
37
37
  minWidth: 120,
38
38
  padding: "8px 14px",
39
39
  borderRadius: 16,
package/src/types.ts CHANGED
@@ -66,7 +66,7 @@ export type ObservableEdge = Edge<ObservableEdgeData>;
66
66
  /**
67
67
  * Node types for the investigation graph view.
68
68
  */
69
- export type InvestigationNodeType = "root" | "check" | "container";
69
+ export type InvestigationNodeType = "root" | "check" | "tag";
70
70
 
71
71
  /**
72
72
  * Data attached to investigation graph nodes.
@@ -74,7 +74,7 @@ export type InvestigationNodeType = "root" | "check" | "container";
74
74
  export interface InvestigationNodeData extends Record<string, unknown> {
75
75
  /** Display label */
76
76
  label: string;
77
- /** Node type (root, check, or container) */
77
+ /** Node type (root, check, or tag) */
78
78
  nodeType: InvestigationNodeType;
79
79
  /** Security level */
80
80
  level: Level;
@@ -82,8 +82,8 @@ export interface InvestigationNodeData extends Record<string, unknown> {
82
82
  score: number;
83
83
  /** Description (for checks) */
84
84
  description?: string;
85
- /** Path (for containers) */
86
- path?: string;
85
+ /** Name (for tags) */
86
+ name?: string;
87
87
  }
88
88
 
89
89
  /**