@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
2
|
+
import { Bug, Crosshair, Globe, Server, X } from "lucide-react";
|
|
3
|
+
import type { SNode, Severity, Surface } from "../lib/surface";
|
|
4
|
+
import { findingTypeMeta, severityMeta } from "../lib/surface";
|
|
5
|
+
import { NodeGlyph } from "./NodeGlyph";
|
|
6
|
+
|
|
7
|
+
const SEV: Severity[] = ["critical", "high", "medium", "low", "info"];
|
|
8
|
+
|
|
9
|
+
export function Inspector({ surface, node, onSelect, onClose }: {
|
|
10
|
+
surface: Surface; node: SNode | null; onSelect: (id: string) => void; onClose: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<aside className="flex w-[340px] shrink-0 flex-col border-l border-line bg-surface">
|
|
14
|
+
<div className="flex h-14 items-center justify-between border-b border-line px-5">
|
|
15
|
+
<div className="flex items-center gap-2.5">
|
|
16
|
+
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-brand-soft"><Crosshair className="h-4 w-4 text-brand" /></div>
|
|
17
|
+
<span className="text-[13px] font-bold tracking-tight text-ink">Inspector</span>
|
|
18
|
+
</div>
|
|
19
|
+
{node && <button onClick={onClose} className="rounded-lg p-1.5 text-faint hover:bg-panel-2 hover:text-ink"><X className="h-4 w-4" /></button>}
|
|
20
|
+
</div>
|
|
21
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
22
|
+
<AnimatePresence mode="wait">
|
|
23
|
+
{!node && <Empty key="empty" />}
|
|
24
|
+
{node?.type === "finding" && <Body key={node.id}><FindingView node={node} surface={surface} onSelect={onSelect} /></Body>}
|
|
25
|
+
{node?.type === "asset" && <Body key={node.id}><AssetView node={node} surface={surface} onSelect={onSelect} /></Body>}
|
|
26
|
+
{node?.type === "domain" && <Body key={node.id}><DomainView node={node} surface={surface} onSelect={onSelect} /></Body>}
|
|
27
|
+
</AnimatePresence>
|
|
28
|
+
</div>
|
|
29
|
+
</aside>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Body({ children }: { children: React.ReactNode }) {
|
|
34
|
+
return <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }} transition={{ duration: 0.2 }} className="p-4">{children}</motion.div>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Header({ color, soft, icon, title, sub }: { color: string; soft: string; icon: React.ReactNode; title: string; sub: string }) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex items-center gap-3 rounded-2xl border border-line bg-panel p-3.5" style={{ boxShadow: `inset 0 0 0 1px ${color}22` }}>
|
|
40
|
+
<div className="flex h-11 w-11 items-center justify-center rounded-xl" style={{ background: soft, color }}>{icon}</div>
|
|
41
|
+
<div className="min-w-0">
|
|
42
|
+
<div className="truncate text-[15px] font-bold text-ink">{title}</div>
|
|
43
|
+
<div className="font-mono text-[10px] uppercase tracking-wider text-faint">{sub}</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Row({ label, value, mono, wrap, accent }: { label: string; value?: string; mono?: boolean; wrap?: boolean; accent?: string }) {
|
|
50
|
+
if (!value) return null;
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex items-start justify-between gap-3 border-b border-line-soft px-3.5 py-2.5 last:border-0">
|
|
53
|
+
<span className="shrink-0 font-mono text-[11px] uppercase tracking-wider text-faint">{label}</span>
|
|
54
|
+
<span className={`text-right text-[12px] ${mono ? "font-mono" : "font-medium"} ${wrap ? "break-all" : "truncate"}`} style={{ color: accent ?? "#c2cbd8" }}>{value}</span>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// vulnerability (minimal)
|
|
60
|
+
function FindingView({ node, surface, onSelect }: { node: SNode; surface: Surface; onSelect: (id: string) => void }) {
|
|
61
|
+
const ft = findingTypeMeta[node.findingType ?? "vulnerability"];
|
|
62
|
+
const sev = severityMeta[node.severity];
|
|
63
|
+
const asset = node.parentAsset ? surface.nodeMap[node.parentAsset] : null;
|
|
64
|
+
return (
|
|
65
|
+
<div className="space-y-3">
|
|
66
|
+
<Header color={ft.css || "#6b7280"} soft={`${ft.css}22`} icon={<Bug className="h-5 w-5" />} title={node.label} sub={ft.label} />
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
<span className="rounded-md px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide" style={{ color: sev.css, background: `${sev.css}1f` ||"#6b7280" }}>{sev.label}</span>
|
|
69
|
+
{node.category && <span className="rounded-md bg-panel-2 px-2 py-0.5 font-mono text-[10px] text-muted">{node.category}</span>}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="rounded-2xl border border-line bg-panel">
|
|
72
|
+
<Row label="Severity" value={sev.label} accent={sev.css} />
|
|
73
|
+
<Row label="Confidence" value={node.confidence} />
|
|
74
|
+
<Row label="Host" value={node.host} mono />
|
|
75
|
+
<Row label="Port" value={String(node.port)} mono />
|
|
76
|
+
<Row label="Template" value={node.templateId} mono wrap />
|
|
77
|
+
</div>
|
|
78
|
+
{node.description && <p className="px-1 text-[12px] leading-relaxed text-ink-2">{node.description}</p>}
|
|
79
|
+
{asset && (
|
|
80
|
+
<button onClick={() => onSelect(asset.id)} className="flex w-full items-center gap-2 rounded-xl border border-line bg-panel px-3 py-2.5 text-left hover:border-brand/40">
|
|
81
|
+
<Server className="h-3.5 w-3.5 text-azure" /><span className="truncate font-mono text-[11px] text-ink">{asset.host}</span>
|
|
82
|
+
<span className="ml-auto font-mono text-[9px] uppercase text-faint">asset</span>
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// asset (minimal)
|
|
90
|
+
function AssetView({ node, surface, onSelect }: { node: SNode; surface: Surface; onSelect: (id: string) => void }) {
|
|
91
|
+
const findings = surface.nodes.filter((n) => n.parentAsset === node.id);
|
|
92
|
+
const dom = node.parentDomain ? surface.nodeMap[node.parentDomain] : null;
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-3">
|
|
95
|
+
<Header color="#46a6ff" soft="#0f2030" icon={<Server className="h-5 w-5" />} title={node.host ?? node.label} sub="Asset" />
|
|
96
|
+
<div className="rounded-2xl border border-line bg-panel">
|
|
97
|
+
<Row label="Host" value={node.host} mono />
|
|
98
|
+
<Row label="Findings" value={String(findings.length)} />
|
|
99
|
+
<Row label="Domain" value={dom?.url} mono />
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<div className="mb-2 px-1 font-mono text-[10px] uppercase tracking-wider text-faint">Findings · {findings.length}</div>
|
|
103
|
+
<div className="space-y-1.5">
|
|
104
|
+
{findings.map((f) => (
|
|
105
|
+
<button key={f.id} onClick={() => onSelect(f.id)} className="group flex w-full items-center gap-2 rounded-lg border border-line bg-panel px-2.5 py-2 text-left hover:border-brand/40">
|
|
106
|
+
<span className="h-2 w-2 shrink-0 rounded-full" style={{ background: severityMeta[f.severity].css }} />
|
|
107
|
+
<span className="truncate text-[11.5px] text-ink-2 group-hover:text-ink">{f.label}</span>
|
|
108
|
+
<span className="ml-auto shrink-0 font-mono text-[9px] uppercase" style={{ color: severityMeta[f.severity].css }}>{severityMeta[f.severity].label}</span>
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
{dom && (
|
|
114
|
+
<button onClick={() => onSelect(dom.id)} className="flex w-full items-center gap-2 rounded-xl border border-line bg-panel px-3 py-2.5 text-left hover:border-brand/40">
|
|
115
|
+
<Globe className="h-3.5 w-3.5 text-brand" /><span className="truncate font-mono text-[11px] text-ink">{dom.domainName}</span>
|
|
116
|
+
<span className="ml-auto font-mono text-[9px] uppercase text-faint">domain</span>
|
|
117
|
+
</button>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// domain (minimal — SOURCETYPE / URL)
|
|
124
|
+
function DomainView({ node, surface, onSelect }: { node: SNode; surface: Surface; onSelect: (id: string) => void }) {
|
|
125
|
+
const dom = surface.domains.find((d) => d.id === node.id)!;
|
|
126
|
+
const total = SEV.reduce((n, s) => n + dom.tally[s], 0) || 1;
|
|
127
|
+
return (
|
|
128
|
+
<div className="space-y-3">
|
|
129
|
+
<Header color="#5b7cfa" soft="#16203a" icon={<NodeGlyph type="domain" className="h-5 w-5" />} title={dom.name} sub="Source" />
|
|
130
|
+
<div className="rounded-2xl border border-line bg-panel">
|
|
131
|
+
<Row label="Sourcetype" value="domain" />
|
|
132
|
+
<Row label="URL" value={dom.url} />
|
|
133
|
+
<Row label="Assets" value={String(dom.assetIds.length)} />
|
|
134
|
+
<Row label="Findings" value={String(dom.findingIds.length)} />
|
|
135
|
+
</div>
|
|
136
|
+
{/* <div className="rounded-2xl border border-line bg-panel p-3">
|
|
137
|
+
<div className="mb-2 font-mono text-[10px] uppercase tracking-wider text-faint">Severity</div>
|
|
138
|
+
<div className="mb-2 flex h-1.5 overflow-hidden rounded-full bg-line">
|
|
139
|
+
{SEV.map((s) => (dom.tally[s] > 0 ? <div key={s} style={{ width: `${(dom.tally[s] / total) * 100}%`, background: severityMeta[s].css }} /> : null))}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
|
142
|
+
{SEV.filter((s) => dom.tally[s] > 0).map((s) => (
|
|
143
|
+
<span key={s} className="flex items-center gap-1 font-mono text-[10px]" style={{ color: severityMeta[s].css }}>
|
|
144
|
+
<span className="h-1.5 w-1.5 rounded-full" style={{ background: severityMeta[s].css }} />{dom.tally[s]} {severityMeta[s].label}
|
|
145
|
+
</span>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div> */}
|
|
149
|
+
<div>
|
|
150
|
+
<div className="mb-2 px-1 font-mono text-[10px] uppercase tracking-wider text-faint">Assets · {dom.assetIds.length}</div>
|
|
151
|
+
<div className="space-y-1.5">
|
|
152
|
+
{dom.assetIds.map((aid) => {
|
|
153
|
+
const a = surface.nodeMap[aid];
|
|
154
|
+
return (
|
|
155
|
+
<button key={aid} onClick={() => onSelect(aid)} className="group flex w-full items-center gap-2 rounded-lg border border-line bg-panel px-2.5 py-2 text-left hover:border-brand/40">
|
|
156
|
+
<Server className="h-3.5 w-3.5 shrink-0" style={{ color: severityMeta[a.severity].css }} />
|
|
157
|
+
<span className="truncate font-mono text-[11px] text-ink group-hover:text-brand">{a.host}</span>
|
|
158
|
+
<span className="ml-auto shrink-0 rounded bg-panel-2 px-1.5 font-mono text-[9px] text-faint tnum">{a.findingCount}</span>
|
|
159
|
+
</button>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function Empty() {
|
|
169
|
+
return (
|
|
170
|
+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="flex h-full flex-col items-center justify-center px-8 text-center">
|
|
171
|
+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-line bg-panel shadow-card"><Crosshair className="h-6 w-6 text-faint" /></div>
|
|
172
|
+
<p className="text-[13px] font-semibold text-ink-2">Nothing selected</p>
|
|
173
|
+
<p className="mt-1.5 text-[12px] leading-relaxed text-muted">Select a domain, asset, or finding in the graph or the domain list to inspect details.</p>
|
|
174
|
+
</motion.div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { Surface } from "../lib/surface";
|
|
3
|
+
import { severityMeta } from "../lib/surface";
|
|
4
|
+
import type { Transform } from "../hooks/usePanZoom";
|
|
5
|
+
|
|
6
|
+
const MW = 186, MH = 110;
|
|
7
|
+
|
|
8
|
+
export function Minimap({ surface, transform, viewport }: { surface: Surface; transform: Transform; viewport: { w: number; h: number } }) {
|
|
9
|
+
const { minX, minY, maxX, maxY } = surface.bounds;
|
|
10
|
+
const pad = 50;
|
|
11
|
+
const gw = maxX - minX + pad * 2, gh = maxY - minY + pad * 2;
|
|
12
|
+
const scale = Math.min(MW / gw, MH / gh);
|
|
13
|
+
const ox = (MW - gw * scale) / 2, oy = (MH - gh * scale) / 2;
|
|
14
|
+
const proj = (x: number, y: number) => ({ x: ox + (x - minX + pad) * scale, y: oy + (y - minY + pad) * scale });
|
|
15
|
+
|
|
16
|
+
const view = useMemo(() => {
|
|
17
|
+
const a = proj(-transform.x / transform.k, -transform.y / transform.k);
|
|
18
|
+
const b = proj((viewport.w - transform.x) / transform.k, (viewport.h - transform.y) / transform.k);
|
|
19
|
+
return { x: a.x, y: a.y, w: b.x - a.x, h: b.y - a.y };
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
}, [transform, viewport]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="overflow-hidden rounded-xl border border-line bg-surface/95 shadow-card backdrop-blur">
|
|
25
|
+
<div className="flex items-center justify-between border-b border-line-soft px-3 py-2">
|
|
26
|
+
<span className="font-mono text-[9px] font-medium uppercase tracking-wider text-faint">Overview</span>
|
|
27
|
+
<span className="font-mono text-[9px] text-faint tnum">{surface.nodes.length}n</span>
|
|
28
|
+
</div>
|
|
29
|
+
<svg width={MW} height={MH} className="block">
|
|
30
|
+
{surface.links.map((l) => {
|
|
31
|
+
const from = surface.nodeMap[l.from];
|
|
32
|
+
const to = surface.nodeMap[l.to];
|
|
33
|
+
if (!from || !to) return null;
|
|
34
|
+
const a = proj(from.x, from.y);
|
|
35
|
+
const b = proj(to.x, to.y);
|
|
36
|
+
return <line key={l.id} x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#1c2431" strokeWidth={0.5} />;
|
|
37
|
+
})}
|
|
38
|
+
{surface.nodes.map((n) => {
|
|
39
|
+
const p = proj(n.x, n.y);
|
|
40
|
+
const color = n.type === "domain" ? "#5b7cfa" : severityMeta[n.severity].css;
|
|
41
|
+
return <circle key={n.id} cx={p.x} cy={p.y} r={n.type === "domain" ? 2.4 : n.type === "asset" ? 1.8 : 1.3} fill={color} />;
|
|
42
|
+
})}
|
|
43
|
+
<rect x={Math.max(0, view.x)} y={Math.max(0, view.y)} width={Math.min(MW, view.w)} height={Math.min(MH, view.h)}
|
|
44
|
+
fill="rgba(91,124,250,0.12)" stroke="#5b7cfa" strokeWidth={1} rx={2} />
|
|
45
|
+
</svg>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Globe, Server, Bug } from "lucide-react";
|
|
2
|
+
import type { NodeType } from "../lib/surface";
|
|
3
|
+
|
|
4
|
+
const map: Record<NodeType, typeof Globe> = { domain: Globe, asset: Server, finding: Bug };
|
|
5
|
+
|
|
6
|
+
export function NodeGlyph({ type, className }: { type: NodeType; className?: string }) {
|
|
7
|
+
const Icon = map[type];
|
|
8
|
+
return <Icon className={className} strokeWidth={2} />;
|
|
9
|
+
}
|