@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 +3 -3
- package/dist/index.cjs +62 -45
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +62 -45
- package/package.json +2 -2
- package/src/components/Icons.tsx +3 -3
- package/src/components/InvestigationGraph.tsx +79 -57
- package/src/components/InvestigationNode.tsx +2 -2
- package/src/types.ts +4 -4
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 →
|
|
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" | "
|
|
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`, `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1675
|
-
|
|
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
|
|
1711
|
-
const
|
|
1712
|
-
for (const
|
|
1713
|
-
for (const checkKey of
|
|
1714
|
-
|
|
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.
|
|
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 (!
|
|
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
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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: `
|
|
1762
|
+
id: `tag-${tagName}`,
|
|
1763
1763
|
type: "investigation",
|
|
1764
1764
|
position: { x: 0, y: 0 },
|
|
1765
|
-
data:
|
|
1765
|
+
data: tagNodeData,
|
|
1766
1766
|
selectable: true,
|
|
1767
1767
|
draggable: true
|
|
1768
1768
|
});
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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-
|
|
1780
|
-
source: `
|
|
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" | "
|
|
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
|
|
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
|
-
/**
|
|
61
|
-
|
|
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
|
|
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" | "
|
|
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
|
|
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
|
-
/**
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1664
|
-
|
|
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
|
|
1700
|
-
const
|
|
1701
|
-
for (const
|
|
1702
|
-
for (const checkKey of
|
|
1703
|
-
|
|
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.
|
|
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 (!
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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: `
|
|
1751
|
+
id: `tag-${tagName}`,
|
|
1752
1752
|
type: "investigation",
|
|
1753
1753
|
position: { x: 0, y: 0 },
|
|
1754
|
-
data:
|
|
1754
|
+
data: tagNodeData,
|
|
1755
1755
|
selectable: true,
|
|
1756
1756
|
draggable: true
|
|
1757
1757
|
});
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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-
|
|
1769
|
-
source: `
|
|
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": "
|
|
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": "
|
|
29
|
+
"@cyvest/cyvest-js": "5.0.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/d3-force": "^3.0.10",
|
package/src/components/Icons.tsx
CHANGED
|
@@ -594,9 +594,9 @@ export const CheckIcon: React.FC<IconProps> = ({
|
|
|
594
594
|
);
|
|
595
595
|
|
|
596
596
|
/**
|
|
597
|
-
*
|
|
597
|
+
* Tag icon for tags
|
|
598
598
|
*/
|
|
599
|
-
export const
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
*
|
|
59
|
+
* Get all tags as an array.
|
|
59
60
|
*/
|
|
60
|
-
function
|
|
61
|
-
|
|
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
|
|
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
|
|
121
|
-
const
|
|
122
|
-
for (const
|
|
123
|
-
for (const checkKey of
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
159
|
-
// Checks in
|
|
160
|
-
if (!
|
|
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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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: `
|
|
187
|
+
id: `tag-${tagName}`,
|
|
186
188
|
type: "investigation",
|
|
187
189
|
position: { x: 0, y: 0 },
|
|
188
|
-
data:
|
|
190
|
+
data: tagNodeData,
|
|
189
191
|
selectable: true,
|
|
190
192
|
draggable: true,
|
|
191
193
|
});
|
|
194
|
+
}
|
|
192
195
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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-
|
|
207
|
-
source: `
|
|
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
|
|
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
|
-
|
|
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" | "
|
|
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
|
|
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
|
-
/**
|
|
86
|
-
|
|
85
|
+
/** Name (for tags) */
|
|
86
|
+
name?: string;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|