@dhruviii/attack-path 1.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/.storybook/main.ts +17 -0
- package/.storybook/preview.tsx +21 -0
- package/README.md +73 -0
- package/dist/attack-path.js +6127 -0
- package/dist/favicon.svg +8 -0
- package/dist/vite.svg +1 -0
- package/eslint.config.js +23 -0
- package/index.html +696 -0
- package/package.json +59 -0
- package/public/favicon.svg +8 -0
- package/public/vite.svg +1 -0
- package/src/App.css +0 -0
- package/src/App.tsx +9 -0
- package/src/Attack-path.tsx +195 -0
- package/src/components/DomainSidebar.tsx +154 -0
- package/src/components/GraphCanvas.tsx +135 -0
- package/src/components/Inspector.tsx +176 -0
- package/src/components/Minimap.tsx +48 -0
- package/src/components/NodeGlyph.tsx +9 -0
- package/src/data/data.json +530 -0
- package/src/hooks/usePanZoom.ts +97 -0
- package/src/index.css +76 -0
- package/src/index.tsx +1 -0
- package/src/lib/surface.ts +367 -0
- package/src/main.tsx +10 -0
- package/src/stories/components/card/card.stories.tsx +32 -0
- package/src/stories/components/card/card.tsx +13 -0
- package/src/stories/components/card/index.ts +1 -0
- package/tsconfig.app.json +20 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +53 -0
- package/vitest.shims.d.ts +1 -0
package/src/index.css
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
/* black / dark theme */
|
|
5
|
+
--color-void: #06080c;
|
|
6
|
+
--color-canvas: #080b11;
|
|
7
|
+
--color-surface: #0c1017;
|
|
8
|
+
--color-panel: #10141d;
|
|
9
|
+
--color-panel-2: #151b26;
|
|
10
|
+
--color-line: #1c2431;
|
|
11
|
+
--color-line-soft: #161d28;
|
|
12
|
+
--color-line-bright: #2a3646;
|
|
13
|
+
|
|
14
|
+
--color-ink: #e8edf5;
|
|
15
|
+
--color-ink-2: #c2cbd8;
|
|
16
|
+
--color-muted: #7f8da3;
|
|
17
|
+
--color-faint: #566274;
|
|
18
|
+
|
|
19
|
+
--color-brand: #5b7cfa;
|
|
20
|
+
--color-brand-soft: #16203a;
|
|
21
|
+
--color-mint: #2dd4a7;
|
|
22
|
+
--color-danger: #ff5d68;
|
|
23
|
+
--color-orange: #ff8f4d;
|
|
24
|
+
--color-amber: #ffc043;
|
|
25
|
+
--color-azure: #46a6ff;
|
|
26
|
+
--color-violet: #a785ff;
|
|
27
|
+
|
|
28
|
+
--font-display: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
29
|
+
--font-mono: "IBM Plex Mono", ui-monospace, monospace;
|
|
30
|
+
|
|
31
|
+
--shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 4px 16px rgba(0,0,0,0.3);
|
|
32
|
+
--shadow-pop: 0 12px 40px rgba(0,0,0,0.55);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
html, body, #root { height: 100%; }
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
background: var(--color-void);
|
|
40
|
+
color: var(--color-ink);
|
|
41
|
+
font-family: var(--font-display);
|
|
42
|
+
-webkit-font-smoothing: antialiased;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
47
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
48
|
+
::-webkit-scrollbar-thumb {
|
|
49
|
+
background: #232d3c;
|
|
50
|
+
border-radius: 999px;
|
|
51
|
+
border: 3px solid transparent;
|
|
52
|
+
background-clip: content-box;
|
|
53
|
+
}
|
|
54
|
+
::-webkit-scrollbar-thumb:hover { background: #2f3b4d; background-clip: content-box; }
|
|
55
|
+
|
|
56
|
+
@keyframes rise {
|
|
57
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
58
|
+
to { opacity: 1; transform: translateY(0); }
|
|
59
|
+
}
|
|
60
|
+
.rise { animation: rise 0.45s cubic-bezier(0.16,1,0.3,1) both; }
|
|
61
|
+
|
|
62
|
+
@keyframes node-in { from { opacity: 0; } to { opacity: 1; } }
|
|
63
|
+
.node-in { animation: node-in 0.4s ease-out both; }
|
|
64
|
+
|
|
65
|
+
.tnum { font-variant-numeric: tabular-nums; }
|
|
66
|
+
|
|
67
|
+
.dot-bg {
|
|
68
|
+
background-color: #080b11;
|
|
69
|
+
background-image: radial-gradient(rgba(91,124,250,0.10) 1px, transparent 1.4px);
|
|
70
|
+
background-size: 26px 26px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.glow-danger { box-shadow: 0 0 0 1px rgba(255,93,104,0.25), 0 0 24px rgba(255,93,104,0.12); }
|
|
74
|
+
|
|
75
|
+
.shadow-card { box-shadow: var(--shadow-card); }
|
|
76
|
+
.shadow-pop { box-shadow: var(--shadow-pop); }
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Attack_path from "./Attack-path"
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Attack-surface model: DOMAIN (source) → ASSET → FINDING (vulnerability)
|
|
3
|
+
// Deterministic. Two layouts: "path" (hierarchical columns) and "graph"
|
|
4
|
+
// (organic radial clusters). SVG-friendly for the dark dashboard.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export type NodeType = "domain" | "asset" | "finding";
|
|
8
|
+
export type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
9
|
+
export type FindingType = "vulnerability" | "misconfiguration" | "exposure" | "secret";
|
|
10
|
+
|
|
11
|
+
export interface SNode {
|
|
12
|
+
id: string;
|
|
13
|
+
type: NodeType;
|
|
14
|
+
label: string;
|
|
15
|
+
severity: Severity;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
// domain
|
|
19
|
+
domainName?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
// asset
|
|
22
|
+
host?: string;
|
|
23
|
+
findingCount?: number;
|
|
24
|
+
// finding
|
|
25
|
+
findingType?: FindingType;
|
|
26
|
+
category?: string;
|
|
27
|
+
confidence?: "high" | "medium" | "low";
|
|
28
|
+
port?: number;
|
|
29
|
+
ip?: string;
|
|
30
|
+
templateId?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
riskScore?: number;
|
|
33
|
+
discoveredAt?: string;
|
|
34
|
+
// links up
|
|
35
|
+
parentDomain?: string;
|
|
36
|
+
parentAsset?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SLink { id: string; from: string; to: string; severity: Severity; }
|
|
40
|
+
|
|
41
|
+
export interface Domain {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string; // "Tata"
|
|
44
|
+
fullName: string; // "Tata Chemicals"
|
|
45
|
+
url: string; // "tatachemicals.com"
|
|
46
|
+
assetIds: string[];
|
|
47
|
+
findingIds: string[];
|
|
48
|
+
tally: Record<Severity, number>;
|
|
49
|
+
risk: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ExposurePath {
|
|
53
|
+
id: string;
|
|
54
|
+
nodes: string[]; // [domain, asset, finding]
|
|
55
|
+
severity: Severity;
|
|
56
|
+
risk: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Surface {
|
|
60
|
+
nodes: SNode[];
|
|
61
|
+
links: SLink[];
|
|
62
|
+
nodeMap: Record<string, SNode>;
|
|
63
|
+
domains: Domain[];
|
|
64
|
+
paths: ExposurePath[];
|
|
65
|
+
out: Record<string, SLink[]>;
|
|
66
|
+
in: Record<string, SLink[]>;
|
|
67
|
+
bounds: { minX: number; minY: number; maxX: number; maxY: number };
|
|
68
|
+
stats: { domains: number; assets: number; findings: number; bySeverity: Record<Severity, number> };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const severityMeta: Record<
|
|
72
|
+
Severity,
|
|
73
|
+
{ label: string; css: string; soft: string; text: string; rank: number }
|
|
74
|
+
> = {
|
|
75
|
+
critical: { label: "Critical", css: "#ff5d68", soft: "#2a1416", text: "text-danger", rank: 4 },
|
|
76
|
+
high: { label: "High", css: "#ff8f4d", soft: "#2a1a10", text: "text-orange", rank: 3 },
|
|
77
|
+
medium: { label: "Medium", css: "#ffc043", soft: "#2a2110", text: "text-amber", rank: 2 },
|
|
78
|
+
low: { label: "Low", css: "#46a6ff", soft: "#0f2030", text: "text-azure", rank: 1 },
|
|
79
|
+
info: { label: "Info", css: "#7f8da3", soft: "#161d28", text: "text-muted", rank: 0 },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const findingTypeMeta: Record<FindingType, { label: string; css: string }> = {
|
|
83
|
+
vulnerability: { label: "Vulnerability", css: "#ff5d68" },
|
|
84
|
+
misconfiguration: { label: "Misconfiguration", css: "#ffc043" },
|
|
85
|
+
exposure: { label: "Exposure Signal", css: "#ff8f4d" },
|
|
86
|
+
secret: { label: "Exposed Secret", css: "#a785ff" },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// ---- layouts ----
|
|
91
|
+
export function pathLayout(s: Surface): Record<string, { x: number; y: number }> {
|
|
92
|
+
const pos: Record<string, { x: number; y: number }> = {};
|
|
93
|
+
const COL_DOMAIN = 0;
|
|
94
|
+
const COL_ASSET = 380;
|
|
95
|
+
const COL_FINDING = 760;
|
|
96
|
+
const ASSET_GAP = 150;
|
|
97
|
+
const FINDING_GAP = 92;
|
|
98
|
+
const BLOCK_GAP = 120;
|
|
99
|
+
let cursorY = 0;
|
|
100
|
+
s.domains.forEach((dom) => {
|
|
101
|
+
const assets = s.nodes.filter((n) => n.type === "asset" && dom.assetIds.includes(n.id));
|
|
102
|
+
const findings = s.nodes.filter((n) => n.type === "finding" && dom.findingIds.includes(n.id));
|
|
103
|
+
const assetCount = Math.max(assets.length, 1);
|
|
104
|
+
const totalF = Math.max(findings.length, 1);
|
|
105
|
+
const blockH = Math.max(assetCount * ASSET_GAP, totalF * FINDING_GAP);
|
|
106
|
+
pos[dom.id] = { x: COL_DOMAIN, y: cursorY + blockH / 2 };
|
|
107
|
+
let fy = 0;
|
|
108
|
+
assets.forEach((an, ai) => {
|
|
109
|
+
const assetY = cursorY + (ai + 0.5) * (blockH / assetCount);
|
|
110
|
+
pos[an.id] = { x: COL_ASSET, y: assetY };
|
|
111
|
+
const aFindings = s.nodes.filter((n) => n.parentAsset === an.id);
|
|
112
|
+
aFindings.forEach((fn) => {
|
|
113
|
+
const findY = cursorY + (fy + 0.5) * FINDING_GAP;
|
|
114
|
+
pos[fn.id] = { x: COL_FINDING, y: findY };
|
|
115
|
+
fy++;
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
cursorY += blockH + BLOCK_GAP;
|
|
119
|
+
});
|
|
120
|
+
return pos;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function graphLayout(s: Surface): Record<string, { x: number; y: number }> {
|
|
124
|
+
const pos: Record<string, { x: number; y: number }> = {};
|
|
125
|
+
const dc = s.domains.length;
|
|
126
|
+
const ring = Math.max(720, dc * 240);
|
|
127
|
+
s.domains.forEach((dom, di) => {
|
|
128
|
+
const ang = (di / dc) * Math.PI * 2 + 0.4;
|
|
129
|
+
const dx = Math.cos(ang) * ring * 0.42;
|
|
130
|
+
const dy = Math.sin(ang) * ring * 0.42;
|
|
131
|
+
pos[dom.id] = { x: dx, y: dy };
|
|
132
|
+
const ac = dom.assetIds.length;
|
|
133
|
+
dom.assetIds.forEach((aid, ai) => {
|
|
134
|
+
const spread = Math.PI * 1.1;
|
|
135
|
+
const aAng = ang - spread / 2 + (ac > 1 ? (ai / (ac - 1)) * spread : spread / 2);
|
|
136
|
+
const ax = dx + Math.cos(aAng) * 240;
|
|
137
|
+
const ay = dy + Math.sin(aAng) * 240;
|
|
138
|
+
pos[aid] = { x: ax, y: ay };
|
|
139
|
+
const findings = s.nodes.filter((n) => n.parentAsset === aid);
|
|
140
|
+
findings.forEach((fn, i) => {
|
|
141
|
+
const angle = i * 0.55;
|
|
142
|
+
const radius = 140 + i * 18;
|
|
143
|
+
|
|
144
|
+
pos[fn.id] = {
|
|
145
|
+
x: ax + Math.cos(angle) * radius,
|
|
146
|
+
y: ay + Math.sin(angle) * radius,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
return pos;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function withPositions(s: Surface, pos: Record<string, { x: number; y: number }>): Surface {
|
|
155
|
+
const nodes = s.nodes.map((n) => ({ ...n, x: pos[n.id]?.x ?? n.x, y: pos[n.id]?.y ?? n.y }));
|
|
156
|
+
const nodeMap: Record<string, SNode> = {};
|
|
157
|
+
nodes.forEach((n) => (nodeMap[n.id] = n));
|
|
158
|
+
const xs = nodes.map((n) => n.x), ys = nodes.map((n) => n.y);
|
|
159
|
+
return { ...s, nodes, nodeMap, bounds: { minX: Math.min(...xs), minY: Math.min(...ys), maxX: Math.max(...xs), maxY: Math.max(...ys) } };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function subtreeOf(id: string, s: Surface): Set<string> {
|
|
163
|
+
const n = s.nodeMap[id];
|
|
164
|
+
const set = new Set<string>([id]);
|
|
165
|
+
if (!n) return set;
|
|
166
|
+
if (n.type === "domain") {
|
|
167
|
+
s.domains.find((d) => d.id === id)?.assetIds.forEach((aid) => {
|
|
168
|
+
set.add(aid);
|
|
169
|
+
s.nodes.filter((f) => f.parentAsset === aid).forEach((f) => set.add(f.id));
|
|
170
|
+
});
|
|
171
|
+
} else if (n.type === "asset") {
|
|
172
|
+
if (n.parentDomain) set.add(n.parentDomain);
|
|
173
|
+
s.nodes.filter((f) => f.parentAsset === id).forEach((f) => set.add(f.id));
|
|
174
|
+
} else {
|
|
175
|
+
if (n.parentAsset) set.add(n.parentAsset);
|
|
176
|
+
if (n.parentDomain) set.add(n.parentDomain);
|
|
177
|
+
}
|
|
178
|
+
return set;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function loadSurfaceFromJSON(rawData: { nodes: any[]; edges: any[] }): Surface {
|
|
182
|
+
const rawNodes = rawData.nodes;
|
|
183
|
+
const rawEdges = rawData.edges;
|
|
184
|
+
const edgeFromMap: Record<string, string[]> = {};
|
|
185
|
+
const edgeToMap: Record<string, string[]> = {};
|
|
186
|
+
rawEdges.forEach((e) => {
|
|
187
|
+
if (!edgeFromMap[e.target]) edgeFromMap[e.target] = [];
|
|
188
|
+
edgeFromMap[e.target].push(e.source);
|
|
189
|
+
if (!edgeToMap[e.source]) edgeToMap[e.source] = [];
|
|
190
|
+
edgeToMap[e.source].push(e.target);
|
|
191
|
+
});
|
|
192
|
+
const rawMap: Record<string, any> = {};
|
|
193
|
+
rawNodes.forEach((n) => (rawMap[n.id] = n));
|
|
194
|
+
const nodes: SNode[] = [];
|
|
195
|
+
const links: SLink[] = [];
|
|
196
|
+
const domains: Domain[] = [];
|
|
197
|
+
const paths: ExposurePath[] = [];
|
|
198
|
+
const domainRawNodes = rawNodes.filter((n) => n.type === "source");
|
|
199
|
+
const assetRawNodes = rawNodes.filter((n) => n.type === "asset");
|
|
200
|
+
const findingRawNodes = rawNodes.filter((n) => n.type === "finding");
|
|
201
|
+
const getParentDomainId = (nodeId: string): string | undefined => {
|
|
202
|
+
const parents = edgeFromMap[nodeId] || [];
|
|
203
|
+
for (const pId of parents) {
|
|
204
|
+
const parentNode = rawMap[pId];
|
|
205
|
+
if (!parentNode) continue;
|
|
206
|
+
if (parentNode.type === "source") return parentNode.id;
|
|
207
|
+
const grandParent = getParentDomainId(pId);
|
|
208
|
+
if (grandParent) return grandParent;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
};
|
|
212
|
+
const getParentAssetId = (nodeId: string): string | undefined => {
|
|
213
|
+
const parents = edgeFromMap[nodeId] || [];
|
|
214
|
+
for (const pId of parents) {
|
|
215
|
+
const parentNode = rawMap[pId];
|
|
216
|
+
if (parentNode && parentNode.type === "asset") return parentNode.id;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
};
|
|
220
|
+
// 1. Findings
|
|
221
|
+
findingRawNodes.forEach((fn) => {
|
|
222
|
+
const d = fn.data || {};
|
|
223
|
+
const parentAsset = getParentAssetId(fn.id);
|
|
224
|
+
const parentDomain = getParentDomainId(fn.id);
|
|
225
|
+
const findNode: SNode = {
|
|
226
|
+
id: fn.id,
|
|
227
|
+
type: "finding",
|
|
228
|
+
label: d.label || "Vulnerability",
|
|
229
|
+
severity: (d.severity?.toLowerCase() as Severity) || "info",
|
|
230
|
+
x: 0,
|
|
231
|
+
y: 0,
|
|
232
|
+
findingType: d.findingType || "vulnerability",
|
|
233
|
+
category: d.attackPathCategory || d.category || "Vulnerability",
|
|
234
|
+
confidence: d.confidence || "high",
|
|
235
|
+
port: d.port ? Number(d.port) : undefined,
|
|
236
|
+
ip: d.ip,
|
|
237
|
+
templateId: d.templateId,
|
|
238
|
+
description: d.description,
|
|
239
|
+
riskScore: d.riskScore ? Number(d.riskScore) : undefined,
|
|
240
|
+
discoveredAt: d.discoveredAt,
|
|
241
|
+
host: d.url ?? d.label,
|
|
242
|
+
url: d.url,
|
|
243
|
+
parentAsset,
|
|
244
|
+
parentDomain,
|
|
245
|
+
};
|
|
246
|
+
nodes.push(findNode);
|
|
247
|
+
});
|
|
248
|
+
// 2. Assets
|
|
249
|
+
assetRawNodes.forEach((an) => {
|
|
250
|
+
const d = an.data || {};
|
|
251
|
+
const parentDomain = getParentDomainId(an.id);
|
|
252
|
+
const childFindings = nodes.filter((n) => n.parentAsset === an.id);
|
|
253
|
+
let worstRank = -1;
|
|
254
|
+
childFindings.forEach((cf) => {
|
|
255
|
+
const rank = severityMeta[cf.severity]?.rank ?? 0;
|
|
256
|
+
if (rank > worstRank) worstRank = rank;
|
|
257
|
+
});
|
|
258
|
+
const assetNode: SNode = {
|
|
259
|
+
id: an.id,
|
|
260
|
+
type: "asset",
|
|
261
|
+
label: d.label ?? d.url,
|
|
262
|
+
severity: (Object.keys(severityMeta) as Severity[]).find(
|
|
263
|
+
(k) => severityMeta[k].rank === Math.max(0, worstRank)
|
|
264
|
+
) ?? "info",
|
|
265
|
+
x: 0,
|
|
266
|
+
y: 0,
|
|
267
|
+
host: d.url ?? d.label,
|
|
268
|
+
findingCount: childFindings.length,
|
|
269
|
+
parentDomain,
|
|
270
|
+
};
|
|
271
|
+
nodes.push(assetNode);
|
|
272
|
+
});
|
|
273
|
+
// 3. Domains
|
|
274
|
+
domainRawNodes.forEach((dn) => {
|
|
275
|
+
const d = dn.data || {};
|
|
276
|
+
const assetIds = (edgeToMap[dn.id] || []).filter((id) => rawMap[id]?.type === "asset");
|
|
277
|
+
const childAssetNodes = nodes.filter((n) => n.type === "asset" && assetIds.includes(n.id));
|
|
278
|
+
const findingIds: string[] = [];
|
|
279
|
+
const tally: Record<Severity, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
280
|
+
childAssetNodes.forEach((an) => {
|
|
281
|
+
const childFindings = nodes.filter((n) => n.parentAsset === an.id);
|
|
282
|
+
childFindings.forEach((cf) => {
|
|
283
|
+
findingIds.push(cf.id);
|
|
284
|
+
tally[cf.severity]++;
|
|
285
|
+
paths.push({
|
|
286
|
+
id: `p-${cf.id}`,
|
|
287
|
+
nodes: [dn.id, an.id, cf.id],
|
|
288
|
+
severity: cf.severity,
|
|
289
|
+
risk: cf.riskScore ?? 50,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
const worstRank = Math.max(
|
|
294
|
+
...Object.keys(tally)
|
|
295
|
+
.filter((s) => tally[s as Severity] > 0)
|
|
296
|
+
.map((s) => severityMeta[s as Severity].rank),
|
|
297
|
+
0
|
|
298
|
+
);
|
|
299
|
+
const domainNode: SNode = {
|
|
300
|
+
id: dn.id,
|
|
301
|
+
type: "domain",
|
|
302
|
+
label: d.label,
|
|
303
|
+
severity: (Object.keys(severityMeta) as Severity[]).find(
|
|
304
|
+
(k) => severityMeta[k].rank === worstRank
|
|
305
|
+
) ?? "info",
|
|
306
|
+
x: 0,
|
|
307
|
+
y: 0,
|
|
308
|
+
domainName: d.label,
|
|
309
|
+
url: d.url || d.label.toLowerCase() + ".com",
|
|
310
|
+
};
|
|
311
|
+
nodes.push(domainNode);
|
|
312
|
+
const risk = tally.critical * 40 + tally.high * 20 + tally.medium * 8 + tally.low * 3 + tally.info;
|
|
313
|
+
domains.push({
|
|
314
|
+
id: dn.id,
|
|
315
|
+
name: d.label,
|
|
316
|
+
fullName: d.label,
|
|
317
|
+
url: d.url || d.label.toLowerCase() + ".com",
|
|
318
|
+
assetIds,
|
|
319
|
+
findingIds,
|
|
320
|
+
tally,
|
|
321
|
+
risk,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
// 4. Links
|
|
325
|
+
rawEdges.forEach((e) => {
|
|
326
|
+
const targetNode = nodes.find((n) => n.id === e.target);
|
|
327
|
+
links.push({
|
|
328
|
+
id: e.id,
|
|
329
|
+
from: e.source,
|
|
330
|
+
to: e.target,
|
|
331
|
+
severity: targetNode ? targetNode.severity : "info",
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
const nodeMap: Record<string, SNode> = {};
|
|
335
|
+
const out: Record<string, SLink[]> = {};
|
|
336
|
+
const inMap: Record<string, SLink[]> = {};
|
|
337
|
+
nodes.forEach((n) => {
|
|
338
|
+
nodeMap[n.id] = n;
|
|
339
|
+
out[n.id] = [];
|
|
340
|
+
inMap[n.id] = [];
|
|
341
|
+
});
|
|
342
|
+
links.forEach((l) => {
|
|
343
|
+
if (out[l.from]) out[l.from].push(l);
|
|
344
|
+
if (inMap[l.to]) inMap[l.to].push(l);
|
|
345
|
+
});
|
|
346
|
+
paths.sort((a, b) => b.risk - a.risk);
|
|
347
|
+
const xs = nodes.map((n) => n.x), ys = nodes.map((n) => n.y);
|
|
348
|
+
const bySeverity: Record<Severity, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
349
|
+
nodes.filter((n) => n.type === "finding").forEach((n) => bySeverity[n.severity]++);
|
|
350
|
+
return {
|
|
351
|
+
nodes,
|
|
352
|
+
links,
|
|
353
|
+
nodeMap,
|
|
354
|
+
domains,
|
|
355
|
+
paths,
|
|
356
|
+
out,
|
|
357
|
+
in: inMap,
|
|
358
|
+
bounds: { minX: Math.min(...xs, 0), minY: Math.min(...ys, 0), maxX: Math.max(...xs, 800), maxY: Math.max(...ys, 600) },
|
|
359
|
+
stats: {
|
|
360
|
+
domains: domains.length,
|
|
361
|
+
assets: nodes.filter((n) => n.type === "asset").length,
|
|
362
|
+
findings: nodes.filter((n) => n.type === "finding").length,
|
|
363
|
+
bySeverity,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {card} from './card'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof card> = {
|
|
5
|
+
title: "components/card",
|
|
6
|
+
component: card,
|
|
7
|
+
argTypes: {
|
|
8
|
+
title: {
|
|
9
|
+
control: {type: "text"}
|
|
10
|
+
},
|
|
11
|
+
discrption: {
|
|
12
|
+
control: {type: "text"}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export default meta
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof card>;
|
|
19
|
+
|
|
20
|
+
export const primary: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
title: "Hello primary card",
|
|
23
|
+
discrption: "This is description"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const secondry: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
title: "Hello secondry card",
|
|
30
|
+
discrption: "This is secondry card"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {card} from './card'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client", "node"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import dts from 'vite-plugin-dts';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
|
9
|
+
import { playwright } from '@vitest/browser-playwright';
|
|
10
|
+
import peerDependencies from './package.json'
|
|
11
|
+
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [react(), dts({exclude: ["**/*.stories.ts","**/*.test.ts"]}), // Automatically generates your TypeScript types
|
|
15
|
+
],
|
|
16
|
+
build: {
|
|
17
|
+
lib: {
|
|
18
|
+
entry: resolve(__dirname, 'src/Attack-path.tsx'),
|
|
19
|
+
formats: ['es'] // ESM format is the modern standard for React libraries
|
|
20
|
+
},
|
|
21
|
+
rollupOptions: {
|
|
22
|
+
external: Object.keys(peerDependencies),
|
|
23
|
+
output: { globals : {
|
|
24
|
+
react: 'react',
|
|
25
|
+
'react-dom': 'react-dom',
|
|
26
|
+
'react/jsx-runtime': 'react/jsx-runtime'
|
|
27
|
+
}},
|
|
28
|
+
// Do not bundle React to avoid version collisions for consumers
|
|
29
|
+
// external: ['react', 'react-dom', 'react/jsx-runtime']
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
test: {
|
|
33
|
+
projects: [{
|
|
34
|
+
extends: true,
|
|
35
|
+
plugins: [
|
|
36
|
+
// The plugin will run tests for the stories defined in your Storybook config
|
|
37
|
+
storybookTest({
|
|
38
|
+
configDir: path.join(dirname, '.storybook')
|
|
39
|
+
})],
|
|
40
|
+
test: {
|
|
41
|
+
name: 'storybook',
|
|
42
|
+
browser: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
headless: true,
|
|
45
|
+
provider: playwright({}),
|
|
46
|
+
instances: [{
|
|
47
|
+
browser: 'chromium'
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}]
|
|
52
|
+
}
|
|
53
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="@vitest/browser-playwright" />
|