@cyvest/cyvest-vis 3.1.0 → 4.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/dist/index.js +41 -51
- package/dist/index.mjs +39 -49
- package/package.json +9 -9
- package/src/components/InvestigationGraph.tsx +13 -6
- package/src/components/ObservablesGraph.tsx +17 -12
- package/src/utils/observables.ts +23 -22
- package/tests/observables.test.ts +4 -5
- package/vitest.config.ts +14 -0
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ var import_react14 = require("react");
|
|
|
41
41
|
var import_react7 = __toESM(require("react"));
|
|
42
42
|
var import_react8 = require("@xyflow/react");
|
|
43
43
|
var import_style = require("@xyflow/react/dist/style.css");
|
|
44
|
-
var
|
|
44
|
+
var import_cyvest_js2 = require("@cyvest/cyvest-js");
|
|
45
45
|
|
|
46
46
|
// src/types.ts
|
|
47
47
|
var DEFAULT_FORCE_CONFIG = {
|
|
@@ -57,6 +57,7 @@ var import_react = require("react");
|
|
|
57
57
|
var import_react2 = require("@xyflow/react");
|
|
58
58
|
|
|
59
59
|
// src/utils/observables.ts
|
|
60
|
+
var import_cyvest_js = require("@cyvest/cyvest-js");
|
|
60
61
|
var OBSERVABLE_EMOJI_MAP = {
|
|
61
62
|
// Network
|
|
62
63
|
"ipv4-addr": "\u{1F310}",
|
|
@@ -129,42 +130,22 @@ function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
|
|
|
129
130
|
return `${value.slice(0, maxLength - 1)}\u2026`;
|
|
130
131
|
}
|
|
131
132
|
function getLevelColor(level) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
MALICIOUS: "#ef4444"
|
|
146
|
-
// red-500
|
|
147
|
-
};
|
|
148
|
-
return colors[level] ?? colors.NONE;
|
|
133
|
+
return (0, import_cyvest_js.getColorForLevel)(level);
|
|
134
|
+
}
|
|
135
|
+
function lightenHexColor(hex, amount) {
|
|
136
|
+
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
137
|
+
if (normalized.length !== 6) {
|
|
138
|
+
return hex;
|
|
139
|
+
}
|
|
140
|
+
const r = parseInt(normalized.slice(0, 2), 16);
|
|
141
|
+
const g = parseInt(normalized.slice(2, 4), 16);
|
|
142
|
+
const b = parseInt(normalized.slice(4, 6), 16);
|
|
143
|
+
const mix = (channel) => Math.max(0, Math.min(255, Math.round(channel + (255 - channel) * amount)));
|
|
144
|
+
const toHex = (channel) => channel.toString(16).padStart(2, "0");
|
|
145
|
+
return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
|
|
149
146
|
}
|
|
150
147
|
function getLevelBackgroundColor(level) {
|
|
151
|
-
|
|
152
|
-
NONE: "#f3f4f6",
|
|
153
|
-
// gray-100
|
|
154
|
-
TRUSTED: "#dcfce7",
|
|
155
|
-
// green-100
|
|
156
|
-
INFO: "#dbeafe",
|
|
157
|
-
// blue-100
|
|
158
|
-
SAFE: "#dcfce7",
|
|
159
|
-
// green-100
|
|
160
|
-
NOTABLE: "#fef9c3",
|
|
161
|
-
// yellow-100
|
|
162
|
-
SUSPICIOUS: "#ffedd5",
|
|
163
|
-
// orange-100
|
|
164
|
-
MALICIOUS: "#fee2e2"
|
|
165
|
-
// red-100
|
|
166
|
-
};
|
|
167
|
-
return colors[level] ?? colors.NONE;
|
|
148
|
+
return lightenHexColor(getLevelColor(level), 0.85);
|
|
168
149
|
}
|
|
169
150
|
var INVESTIGATION_NODE_EMOJI = {
|
|
170
151
|
root: "\u{1F3AF}",
|
|
@@ -554,7 +535,7 @@ var edgeTypes = {
|
|
|
554
535
|
floating: FloatingEdge
|
|
555
536
|
};
|
|
556
537
|
function createObservableNodes(investigation, rootObservableIds) {
|
|
557
|
-
const graph = (0,
|
|
538
|
+
const graph = (0, import_cyvest_js2.getObservableGraph)(investigation);
|
|
558
539
|
return graph.nodes.map((graphNode, index) => {
|
|
559
540
|
const isRoot = rootObservableIds.has(graphNode.id);
|
|
560
541
|
const shape = getObservableShape(graphNode.type, isRoot);
|
|
@@ -584,7 +565,7 @@ function createObservableNodes(investigation, rootObservableIds) {
|
|
|
584
565
|
});
|
|
585
566
|
}
|
|
586
567
|
function createObservableEdges(investigation) {
|
|
587
|
-
const graph = (0,
|
|
568
|
+
const graph = (0, import_cyvest_js2.getObservableGraph)(investigation);
|
|
588
569
|
return graph.edges.map((graphEdge, index) => {
|
|
589
570
|
const edgeData = {
|
|
590
571
|
relationshipType: graphEdge.type,
|
|
@@ -794,19 +775,25 @@ var ObservablesGraphInner = ({
|
|
|
794
775
|
};
|
|
795
776
|
var ObservablesGraph = (props) => {
|
|
796
777
|
const { investigation } = props;
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
803
|
-
|
|
778
|
+
const { rootKeys, primaryRootId } = (0, import_react7.useMemo)(() => {
|
|
779
|
+
const rootType = investigation.data_extraction.root_type;
|
|
780
|
+
if (!rootType) {
|
|
781
|
+
return { rootKeys: /* @__PURE__ */ new Set(), primaryRootId: void 0 };
|
|
782
|
+
}
|
|
783
|
+
const normalizedRootType = rootType.toLowerCase().trim();
|
|
784
|
+
const rootsByType = Object.values(investigation.observables).filter(
|
|
785
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
786
|
+
);
|
|
787
|
+
return {
|
|
788
|
+
rootKeys: new Set(rootsByType.map((obs) => obs.key)),
|
|
789
|
+
primaryRootId: rootsByType[0]?.key
|
|
790
|
+
};
|
|
804
791
|
}, [investigation]);
|
|
805
792
|
const { initialNodes, initialEdges } = (0, import_react7.useMemo)(() => {
|
|
806
|
-
const nodes = createObservableNodes(investigation,
|
|
793
|
+
const nodes = createObservableNodes(investigation, rootKeys);
|
|
807
794
|
const edges = createObservableEdges(investigation);
|
|
808
795
|
return { initialNodes: nodes, initialEdges: edges };
|
|
809
|
-
}, [investigation,
|
|
796
|
+
}, [investigation, rootKeys]);
|
|
810
797
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react8.ReactFlowProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
811
798
|
ObservablesGraphInner,
|
|
812
799
|
{
|
|
@@ -822,7 +809,6 @@ var ObservablesGraph = (props) => {
|
|
|
822
809
|
var import_react12 = __toESM(require("react"));
|
|
823
810
|
var import_react13 = require("@xyflow/react");
|
|
824
811
|
var import_style2 = require("@xyflow/react/dist/style.css");
|
|
825
|
-
var import_cyvest_js2 = require("@cyvest/cyvest-js");
|
|
826
812
|
|
|
827
813
|
// src/components/InvestigationNode.tsx
|
|
828
814
|
var import_react9 = require("react");
|
|
@@ -1028,10 +1014,14 @@ function flattenContainers(containers) {
|
|
|
1028
1014
|
function createInvestigationGraph(investigation) {
|
|
1029
1015
|
const nodes = [];
|
|
1030
1016
|
const edges = [];
|
|
1031
|
-
const
|
|
1032
|
-
const
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1017
|
+
const rootType = investigation.data_extraction.root_type;
|
|
1018
|
+
const normalizedRootType = rootType?.toLowerCase().trim();
|
|
1019
|
+
const rootsByType = normalizedRootType ? Object.values(investigation.observables).filter(
|
|
1020
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
1021
|
+
) : [];
|
|
1022
|
+
const primaryRoot = rootsByType[0] ?? null;
|
|
1023
|
+
const rootKey = primaryRoot?.key ?? investigation.investigation_id;
|
|
1024
|
+
const rootValue = primaryRoot?.value ?? investigation.investigation_name ?? investigation.investigation_id;
|
|
1035
1025
|
const rootLevel = primaryRoot?.level ?? investigation.level;
|
|
1036
1026
|
const rootNodeData = {
|
|
1037
1027
|
label: truncateLabel(rootValue, 24),
|
package/dist/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
ConnectionMode
|
|
15
15
|
} from "@xyflow/react";
|
|
16
16
|
import "@xyflow/react/dist/style.css";
|
|
17
|
-
import { getObservableGraph
|
|
17
|
+
import { getObservableGraph } from "@cyvest/cyvest-js";
|
|
18
18
|
|
|
19
19
|
// src/types.ts
|
|
20
20
|
var DEFAULT_FORCE_CONFIG = {
|
|
@@ -30,6 +30,7 @@ import { memo } from "react";
|
|
|
30
30
|
import { Handle, Position } from "@xyflow/react";
|
|
31
31
|
|
|
32
32
|
// src/utils/observables.ts
|
|
33
|
+
import { getColorForLevel } from "@cyvest/cyvest-js";
|
|
33
34
|
var OBSERVABLE_EMOJI_MAP = {
|
|
34
35
|
// Network
|
|
35
36
|
"ipv4-addr": "\u{1F310}",
|
|
@@ -102,42 +103,22 @@ function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
|
|
|
102
103
|
return `${value.slice(0, maxLength - 1)}\u2026`;
|
|
103
104
|
}
|
|
104
105
|
function getLevelColor(level) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
MALICIOUS: "#ef4444"
|
|
119
|
-
// red-500
|
|
120
|
-
};
|
|
121
|
-
return colors[level] ?? colors.NONE;
|
|
106
|
+
return getColorForLevel(level);
|
|
107
|
+
}
|
|
108
|
+
function lightenHexColor(hex, amount) {
|
|
109
|
+
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
110
|
+
if (normalized.length !== 6) {
|
|
111
|
+
return hex;
|
|
112
|
+
}
|
|
113
|
+
const r = parseInt(normalized.slice(0, 2), 16);
|
|
114
|
+
const g = parseInt(normalized.slice(2, 4), 16);
|
|
115
|
+
const b = parseInt(normalized.slice(4, 6), 16);
|
|
116
|
+
const mix = (channel) => Math.max(0, Math.min(255, Math.round(channel + (255 - channel) * amount)));
|
|
117
|
+
const toHex = (channel) => channel.toString(16).padStart(2, "0");
|
|
118
|
+
return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
|
|
122
119
|
}
|
|
123
120
|
function getLevelBackgroundColor(level) {
|
|
124
|
-
|
|
125
|
-
NONE: "#f3f4f6",
|
|
126
|
-
// gray-100
|
|
127
|
-
TRUSTED: "#dcfce7",
|
|
128
|
-
// green-100
|
|
129
|
-
INFO: "#dbeafe",
|
|
130
|
-
// blue-100
|
|
131
|
-
SAFE: "#dcfce7",
|
|
132
|
-
// green-100
|
|
133
|
-
NOTABLE: "#fef9c3",
|
|
134
|
-
// yellow-100
|
|
135
|
-
SUSPICIOUS: "#ffedd5",
|
|
136
|
-
// orange-100
|
|
137
|
-
MALICIOUS: "#fee2e2"
|
|
138
|
-
// red-100
|
|
139
|
-
};
|
|
140
|
-
return colors[level] ?? colors.NONE;
|
|
121
|
+
return lightenHexColor(getLevelColor(level), 0.85);
|
|
141
122
|
}
|
|
142
123
|
var INVESTIGATION_NODE_EMOJI = {
|
|
143
124
|
root: "\u{1F3AF}",
|
|
@@ -779,19 +760,25 @@ var ObservablesGraphInner = ({
|
|
|
779
760
|
};
|
|
780
761
|
var ObservablesGraph = (props) => {
|
|
781
762
|
const { investigation } = props;
|
|
782
|
-
const
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
763
|
+
const { rootKeys, primaryRootId } = useMemo2(() => {
|
|
764
|
+
const rootType = investigation.data_extraction.root_type;
|
|
765
|
+
if (!rootType) {
|
|
766
|
+
return { rootKeys: /* @__PURE__ */ new Set(), primaryRootId: void 0 };
|
|
767
|
+
}
|
|
768
|
+
const normalizedRootType = rootType.toLowerCase().trim();
|
|
769
|
+
const rootsByType = Object.values(investigation.observables).filter(
|
|
770
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
771
|
+
);
|
|
772
|
+
return {
|
|
773
|
+
rootKeys: new Set(rootsByType.map((obs) => obs.key)),
|
|
774
|
+
primaryRootId: rootsByType[0]?.key
|
|
775
|
+
};
|
|
789
776
|
}, [investigation]);
|
|
790
777
|
const { initialNodes, initialEdges } = useMemo2(() => {
|
|
791
|
-
const nodes = createObservableNodes(investigation,
|
|
778
|
+
const nodes = createObservableNodes(investigation, rootKeys);
|
|
792
779
|
const edges = createObservableEdges(investigation);
|
|
793
780
|
return { initialNodes: nodes, initialEdges: edges };
|
|
794
|
-
}, [investigation,
|
|
781
|
+
}, [investigation, rootKeys]);
|
|
795
782
|
return /* @__PURE__ */ jsx3(ReactFlowProvider, { children: /* @__PURE__ */ jsx3(
|
|
796
783
|
ObservablesGraphInner,
|
|
797
784
|
{
|
|
@@ -814,7 +801,6 @@ import {
|
|
|
814
801
|
useEdgesState as useEdgesState2
|
|
815
802
|
} from "@xyflow/react";
|
|
816
803
|
import "@xyflow/react/dist/style.css";
|
|
817
|
-
import { findRootObservables as findRootObservables2 } from "@cyvest/cyvest-js";
|
|
818
804
|
|
|
819
805
|
// src/components/InvestigationNode.tsx
|
|
820
806
|
import { memo as memo3 } from "react";
|
|
@@ -1020,10 +1006,14 @@ function flattenContainers(containers) {
|
|
|
1020
1006
|
function createInvestigationGraph(investigation) {
|
|
1021
1007
|
const nodes = [];
|
|
1022
1008
|
const edges = [];
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1009
|
+
const rootType = investigation.data_extraction.root_type;
|
|
1010
|
+
const normalizedRootType = rootType?.toLowerCase().trim();
|
|
1011
|
+
const rootsByType = normalizedRootType ? Object.values(investigation.observables).filter(
|
|
1012
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
1013
|
+
) : [];
|
|
1014
|
+
const primaryRoot = rootsByType[0] ?? null;
|
|
1015
|
+
const rootKey = primaryRoot?.key ?? investigation.investigation_id;
|
|
1016
|
+
const rootValue = primaryRoot?.value ?? investigation.investigation_name ?? investigation.investigation_id;
|
|
1027
1017
|
const rootLevel = primaryRoot?.level ?? investigation.level;
|
|
1028
1018
|
const rootNodeData = {
|
|
1029
1019
|
label: truncateLabel(rootValue, 24),
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyvest/cyvest-vis",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"main": "dist/index.cjs",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"peerDependencies": {
|
|
9
|
-
"react": "^
|
|
10
|
-
"react-dom": "^
|
|
9
|
+
"react": "^19.0.0",
|
|
10
|
+
"react-dom": "^19.0.0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@dagrejs/dagre": "^1.1.
|
|
14
|
-
"@xyflow/react": "^12.
|
|
13
|
+
"@dagrejs/dagre": "^1.1.8",
|
|
14
|
+
"@xyflow/react": "^12.10.0",
|
|
15
15
|
"d3-force": "^3.0.0",
|
|
16
|
-
"@cyvest/cyvest-js": "
|
|
16
|
+
"@cyvest/cyvest-js": "4.0.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/d3-force": "^3.0.10",
|
|
20
20
|
"@types/react": "^19.2.7",
|
|
21
21
|
"@types/react-dom": "^19.2.3",
|
|
22
|
-
"tsup": "^8.
|
|
23
|
-
"typescript": "^5.
|
|
24
|
-
"vitest": "^
|
|
22
|
+
"tsup": "^8.5.1",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^4.0.16"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=20"
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
import "@xyflow/react/dist/style.css";
|
|
19
19
|
|
|
20
20
|
import type { CyvestInvestigation, Check, Container } from "@cyvest/cyvest-js";
|
|
21
|
-
import { findRootObservables } from "@cyvest/cyvest-js";
|
|
22
21
|
|
|
23
22
|
import type {
|
|
24
23
|
InvestigationGraphProps,
|
|
@@ -67,13 +66,21 @@ function createInvestigationGraph(
|
|
|
67
66
|
const nodes: Node<InvestigationNodeData>[] = [];
|
|
68
67
|
const edges: Edge[] = [];
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
69
|
+
const rootType = investigation.data_extraction.root_type;
|
|
70
|
+
const normalizedRootType = rootType?.toLowerCase().trim();
|
|
71
|
+
const rootsByType = normalizedRootType
|
|
72
|
+
? Object.values(investigation.observables).filter(
|
|
73
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
74
|
+
)
|
|
75
|
+
: [];
|
|
76
|
+
const primaryRoot = rootsByType[0] ?? null;
|
|
73
77
|
|
|
74
78
|
// If no root found, use the first observable or create a placeholder
|
|
75
|
-
const rootKey = primaryRoot?.key ??
|
|
76
|
-
const rootValue =
|
|
79
|
+
const rootKey = primaryRoot?.key ?? investigation.investigation_id;
|
|
80
|
+
const rootValue =
|
|
81
|
+
primaryRoot?.value ??
|
|
82
|
+
investigation.investigation_name ??
|
|
83
|
+
investigation.investigation_id;
|
|
77
84
|
const rootLevel = primaryRoot?.level ?? investigation.level;
|
|
78
85
|
|
|
79
86
|
// Create root node
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import "@xyflow/react/dist/style.css";
|
|
22
22
|
|
|
23
23
|
import type { CyvestInvestigation } from "@cyvest/cyvest-js";
|
|
24
|
-
import { getObservableGraph
|
|
24
|
+
import { getObservableGraph } from "@cyvest/cyvest-js";
|
|
25
25
|
|
|
26
26
|
import type {
|
|
27
27
|
ObservablesGraphProps,
|
|
@@ -344,24 +344,29 @@ const ObservablesGraphInner: React.FC<
|
|
|
344
344
|
export const ObservablesGraph: React.FC<ObservablesGraphProps> = (props) => {
|
|
345
345
|
const { investigation } = props;
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
const { rootKeys, primaryRootId } = useMemo(() => {
|
|
348
|
+
const rootType = investigation.data_extraction.root_type;
|
|
349
|
+
if (!rootType) {
|
|
350
|
+
return { rootKeys: new Set<string>(), primaryRootId: undefined };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const normalizedRootType = rootType.toLowerCase().trim();
|
|
354
|
+
const rootsByType = Object.values(investigation.observables).filter(
|
|
355
|
+
(obs) => obs.type.toLowerCase() === normalizedRootType
|
|
356
|
+
);
|
|
352
357
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
358
|
+
return {
|
|
359
|
+
rootKeys: new Set(rootsByType.map((obs) => obs.key)),
|
|
360
|
+
primaryRootId: rootsByType[0]?.key,
|
|
361
|
+
};
|
|
357
362
|
}, [investigation]);
|
|
358
363
|
|
|
359
364
|
// Create initial nodes and edges
|
|
360
365
|
const { initialNodes, initialEdges } = useMemo(() => {
|
|
361
|
-
const nodes = createObservableNodes(investigation,
|
|
366
|
+
const nodes = createObservableNodes(investigation, rootKeys);
|
|
362
367
|
const edges = createObservableEdges(investigation);
|
|
363
368
|
return { initialNodes: nodes, initialEdges: edges };
|
|
364
|
-
}, [investigation,
|
|
369
|
+
}, [investigation, rootKeys]);
|
|
365
370
|
|
|
366
371
|
return (
|
|
367
372
|
<ReactFlowProvider>
|
package/src/utils/observables.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Utility functions for observable visualization.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { getColorForLevel, type Level } from "@cyvest/cyvest-js";
|
|
5
6
|
import type { ObservableShape } from "../types";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -118,33 +119,33 @@ export function truncateLabel(
|
|
|
118
119
|
/**
|
|
119
120
|
* Get color for security level.
|
|
120
121
|
*/
|
|
121
|
-
export function getLevelColor(level:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
export function getLevelColor(level: Level): string {
|
|
123
|
+
return getColorForLevel(level);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function lightenHexColor(hex: string, amount: number): string {
|
|
127
|
+
const normalized = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
128
|
+
if (normalized.length !== 6) {
|
|
129
|
+
return hex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const r = parseInt(normalized.slice(0, 2), 16);
|
|
133
|
+
const g = parseInt(normalized.slice(2, 4), 16);
|
|
134
|
+
const b = parseInt(normalized.slice(4, 6), 16);
|
|
135
|
+
|
|
136
|
+
const mix = (channel: number) =>
|
|
137
|
+
Math.max(0, Math.min(255, Math.round(channel + (255 - channel) * amount)));
|
|
138
|
+
|
|
139
|
+
const toHex = (channel: number) => channel.toString(16).padStart(2, "0");
|
|
140
|
+
|
|
141
|
+
return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
/**
|
|
135
145
|
* Get background color for security level (lighter version).
|
|
136
146
|
*/
|
|
137
|
-
export function getLevelBackgroundColor(level:
|
|
138
|
-
|
|
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;
|
|
147
|
+
export function getLevelBackgroundColor(level: Level): string {
|
|
148
|
+
return lightenHexColor(getLevelColor(level), 0.85);
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
+
import { LEVEL_COLORS } from "@cyvest/cyvest-js";
|
|
3
4
|
import {
|
|
4
5
|
getInvestigationNodeEmoji,
|
|
5
6
|
getLevelBackgroundColor,
|
|
@@ -29,11 +30,9 @@ describe("observables utils", () => {
|
|
|
29
30
|
expect(truncateLabel("averyverylongvalue", 10, false)).toBe("averyvery…");
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
it("maps levels to colors
|
|
33
|
-
expect(getLevelColor("SUSPICIOUS")).toBe(
|
|
34
|
-
expect(
|
|
35
|
-
expect(getLevelBackgroundColor("SUSPICIOUS")).toBe("#ffedd5");
|
|
36
|
-
expect(getLevelBackgroundColor("UNKNOWN")).toBe("#f3f4f6");
|
|
33
|
+
it("maps levels to colors", () => {
|
|
34
|
+
expect(getLevelColor("SUSPICIOUS")).toBe(LEVEL_COLORS.SUSPICIOUS);
|
|
35
|
+
expect(getLevelBackgroundColor("SUSPICIOUS")).toBe("#feeadc");
|
|
37
36
|
});
|
|
38
37
|
|
|
39
38
|
it("returns investigation node emoji with fallback", () => {
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
"@cyvest/cyvest-js": resolve(__dirname, "../cyvest-js/src/index.ts"),
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
test: {
|
|
11
|
+
include: ["tests/**/*.{test,spec}.{ts,js,tsx,jsx}"],
|
|
12
|
+
name: "cyvest-vis",
|
|
13
|
+
},
|
|
14
|
+
});
|