@cyvest/cyvest-vis 2.0.1
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 +42 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +1293 -0
- package/dist/index.mjs +1284 -0
- package/package.json +34 -0
- package/src/components/CyvestGraph.tsx +117 -0
- package/src/components/FloatingEdge.tsx +43 -0
- package/src/components/InvestigationGraph.tsx +257 -0
- package/src/components/InvestigationNode.tsx +145 -0
- package/src/components/ObservableNode.tsx +173 -0
- package/src/components/ObservablesGraph.tsx +376 -0
- package/src/hooks/useDagreLayout.ts +102 -0
- package/src/hooks/useForceLayout.ts +383 -0
- package/src/index.ts +13 -0
- package/src/types.ts +193 -0
- package/src/utils/observables.ts +164 -0
- package/tests/observables.test.ts +43 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for observable visualization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ObservableShape } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Map observable types to emojis.
|
|
9
|
+
*/
|
|
10
|
+
const OBSERVABLE_EMOJI_MAP: Record<string, string> = {
|
|
11
|
+
// Network
|
|
12
|
+
"ipv4-addr": "π",
|
|
13
|
+
"ipv6-addr": "π",
|
|
14
|
+
"domain-name": "π ",
|
|
15
|
+
url: "π",
|
|
16
|
+
"autonomous-system": "π",
|
|
17
|
+
"mac-addr": "πΆ",
|
|
18
|
+
|
|
19
|
+
// Email
|
|
20
|
+
"email-addr": "π§",
|
|
21
|
+
"email-message": "βοΈ",
|
|
22
|
+
|
|
23
|
+
// File
|
|
24
|
+
file: "π",
|
|
25
|
+
"file-hash": "π",
|
|
26
|
+
"file:hash:md5": "π",
|
|
27
|
+
"file:hash:sha1": "π",
|
|
28
|
+
"file:hash:sha256": "π",
|
|
29
|
+
|
|
30
|
+
// User/Identity
|
|
31
|
+
user: "π€",
|
|
32
|
+
"user-account": "π€",
|
|
33
|
+
identity: "πͺͺ",
|
|
34
|
+
|
|
35
|
+
// Process/System
|
|
36
|
+
process: "βοΈ",
|
|
37
|
+
software: "πΏ",
|
|
38
|
+
"windows-registry-key": "π",
|
|
39
|
+
|
|
40
|
+
// Threat Intelligence
|
|
41
|
+
"threat-actor": "πΉ",
|
|
42
|
+
malware: "π¦ ",
|
|
43
|
+
"attack-pattern": "βοΈ",
|
|
44
|
+
campaign: "π―",
|
|
45
|
+
indicator: "π¨",
|
|
46
|
+
|
|
47
|
+
// Artifacts
|
|
48
|
+
artifact: "π§ͺ",
|
|
49
|
+
certificate: "π",
|
|
50
|
+
"x509-certificate": "π",
|
|
51
|
+
|
|
52
|
+
// Default
|
|
53
|
+
unknown: "β",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the emoji for an observable type.
|
|
58
|
+
* Falls back to a generic icon if type is unknown.
|
|
59
|
+
*/
|
|
60
|
+
export function getObservableEmoji(observableType: string): string {
|
|
61
|
+
const normalized = observableType.toLowerCase().trim();
|
|
62
|
+
return OBSERVABLE_EMOJI_MAP[normalized] ?? OBSERVABLE_EMOJI_MAP.unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Map observable types to shapes.
|
|
67
|
+
*/
|
|
68
|
+
const OBSERVABLE_SHAPE_MAP: Record<string, ObservableShape> = {
|
|
69
|
+
// Domains get squares
|
|
70
|
+
"domain-name": "square",
|
|
71
|
+
|
|
72
|
+
// URLs get circles
|
|
73
|
+
url: "circle",
|
|
74
|
+
|
|
75
|
+
// IPs get triangles
|
|
76
|
+
"ipv4-addr": "triangle",
|
|
77
|
+
"ipv6-addr": "triangle",
|
|
78
|
+
|
|
79
|
+
// Root/files get rectangles (default for root)
|
|
80
|
+
file: "rectangle",
|
|
81
|
+
"email-message": "rectangle",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the shape for an observable type.
|
|
86
|
+
*/
|
|
87
|
+
export function getObservableShape(
|
|
88
|
+
observableType: string,
|
|
89
|
+
isRoot: boolean
|
|
90
|
+
): ObservableShape {
|
|
91
|
+
if (isRoot) {
|
|
92
|
+
return "rectangle";
|
|
93
|
+
}
|
|
94
|
+
const normalized = observableType.toLowerCase().trim();
|
|
95
|
+
return OBSERVABLE_SHAPE_MAP[normalized] ?? "circle";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Truncate a label, optionally in the middle for long strings.
|
|
100
|
+
*/
|
|
101
|
+
export function truncateLabel(
|
|
102
|
+
value: string,
|
|
103
|
+
maxLength: number = 20,
|
|
104
|
+
truncateMiddle: boolean = true
|
|
105
|
+
): string {
|
|
106
|
+
if (value.length <= maxLength) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (truncateMiddle) {
|
|
111
|
+
const halfLen = Math.floor((maxLength - 3) / 2);
|
|
112
|
+
return `${value.slice(0, halfLen)}β¦${value.slice(-halfLen)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `${value.slice(0, maxLength - 1)}β¦`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get color for security level.
|
|
120
|
+
*/
|
|
121
|
+
export function getLevelColor(level: string): string {
|
|
122
|
+
const colors: Record<string, string> = {
|
|
123
|
+
NONE: "#6b7280", // gray-500
|
|
124
|
+
TRUSTED: "#22c55e", // green-500
|
|
125
|
+
INFO: "#3b82f6", // blue-500
|
|
126
|
+
SAFE: "#22c55e", // green-500
|
|
127
|
+
NOTABLE: "#eab308", // yellow-500
|
|
128
|
+
SUSPICIOUS: "#f97316", // orange-500
|
|
129
|
+
MALICIOUS: "#ef4444", // red-500
|
|
130
|
+
};
|
|
131
|
+
return colors[level] ?? colors.NONE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get background color for security level (lighter version).
|
|
136
|
+
*/
|
|
137
|
+
export function getLevelBackgroundColor(level: string): string {
|
|
138
|
+
const colors: Record<string, string> = {
|
|
139
|
+
NONE: "#f3f4f6", // gray-100
|
|
140
|
+
TRUSTED: "#dcfce7", // green-100
|
|
141
|
+
INFO: "#dbeafe", // blue-100
|
|
142
|
+
SAFE: "#dcfce7", // green-100
|
|
143
|
+
NOTABLE: "#fef9c3", // yellow-100
|
|
144
|
+
SUSPICIOUS: "#ffedd5", // orange-100
|
|
145
|
+
MALICIOUS: "#fee2e2", // red-100
|
|
146
|
+
};
|
|
147
|
+
return colors[level] ?? colors.NONE;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Emoji map for investigation node types.
|
|
152
|
+
*/
|
|
153
|
+
const INVESTIGATION_NODE_EMOJI: Record<string, string> = {
|
|
154
|
+
root: "π―",
|
|
155
|
+
check: "β
",
|
|
156
|
+
container: "π¦",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get emoji for investigation node type.
|
|
161
|
+
*/
|
|
162
|
+
export function getInvestigationNodeEmoji(nodeType: string): string {
|
|
163
|
+
return INVESTIGATION_NODE_EMOJI[nodeType] ?? "β";
|
|
164
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getInvestigationNodeEmoji,
|
|
5
|
+
getLevelBackgroundColor,
|
|
6
|
+
getLevelColor,
|
|
7
|
+
getObservableEmoji,
|
|
8
|
+
getObservableShape,
|
|
9
|
+
truncateLabel,
|
|
10
|
+
} from "../src/utils/observables";
|
|
11
|
+
|
|
12
|
+
describe("observables utils", () => {
|
|
13
|
+
it("returns emojis for known and unknown observable types", () => {
|
|
14
|
+
expect(getObservableEmoji("domain-name")).toBe("π ");
|
|
15
|
+
expect(getObservableEmoji("IPv4-Addr")).toBe("π");
|
|
16
|
+
expect(getObservableEmoji("unmapped")).toBe("β");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns shapes based on type and root flag", () => {
|
|
20
|
+
expect(getObservableShape("domain-name", false)).toBe("square");
|
|
21
|
+
expect(getObservableShape("ipv6-addr", false)).toBe("triangle");
|
|
22
|
+
expect(getObservableShape("anything-else", false)).toBe("circle");
|
|
23
|
+
expect(getObservableShape("anything-else", true)).toBe("rectangle");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("truncates long labels in the middle by default", () => {
|
|
27
|
+
expect(truncateLabel("short", 10)).toBe("short");
|
|
28
|
+
expect(truncateLabel("averyverylongvalue", 10)).toBe("aveβ¦lue");
|
|
29
|
+
expect(truncateLabel("averyverylongvalue", 10, false)).toBe("averyveryβ¦");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("maps levels to colors and falls back to NONE", () => {
|
|
33
|
+
expect(getLevelColor("SUSPICIOUS")).toBe("#f97316");
|
|
34
|
+
expect(getLevelColor("UNKNOWN")).toBe("#6b7280");
|
|
35
|
+
expect(getLevelBackgroundColor("SUSPICIOUS")).toBe("#ffedd5");
|
|
36
|
+
expect(getLevelBackgroundColor("UNKNOWN")).toBe("#f3f4f6");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns investigation node emoji with fallback", () => {
|
|
40
|
+
expect(getInvestigationNodeEmoji("root")).toBe("π―");
|
|
41
|
+
expect(getInvestigationNodeEmoji("missing")).toBe("β");
|
|
42
|
+
});
|
|
43
|
+
});
|
package/tsconfig.json
ADDED