proxy2model 2.0.0__py3-none-any.whl
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.
- proxy2model/__init__.py +19 -0
- proxy2model/__main__.py +6 -0
- proxy2model/atomic.py +68 -0
- proxy2model/cli/__init__.py +3 -0
- proxy2model/cli/analyze.py +501 -0
- proxy2model/cli/app.py +408 -0
- proxy2model/cli/browse.py +279 -0
- proxy2model/cli/build.py +399 -0
- proxy2model/cli/common.py +190 -0
- proxy2model/cli/history.py +224 -0
- proxy2model/cli/mcp.py +184 -0
- proxy2model/cli/ui.py +62 -0
- proxy2model/core/__init__.py +1 -0
- proxy2model/core/changes.py +124 -0
- proxy2model/core/context.py +571 -0
- proxy2model/core/diff.py +330 -0
- proxy2model/core/domains.py +98 -0
- proxy2model/core/graph.py +562 -0
- proxy2model/core/methodology.py +343 -0
- proxy2model/core/model.py +998 -0
- proxy2model/core/stack.py +218 -0
- proxy2model/data/fonts/LICENSES.md +16 -0
- proxy2model/data/fonts/inter-latin-wght-normal.woff2 +0 -0
- proxy2model/data/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- proxy2model/data/fonts/nippo-200.woff2 +0 -0
- proxy2model/data/fonts/nippo-300.woff2 +0 -0
- proxy2model/data/fonts/nippo-400.woff2 +0 -0
- proxy2model/data/fonts/nippo-500.woff2 +0 -0
- proxy2model/data/fonts/nippo-700.woff2 +0 -0
- proxy2model/data/public_suffix_list.dat +16497 -0
- proxy2model/db/__init__.py +1 -0
- proxy2model/db/bql.py +710 -0
- proxy2model/db/bql_js.py +278 -0
- proxy2model/db/store.py +547 -0
- proxy2model/export/__init__.py +1 -0
- proxy2model/export/openapi.py +248 -0
- proxy2model/ingest/__init__.py +1 -0
- proxy2model/ingest/browse.py +181 -0
- proxy2model/ingest/browser.py +1058 -0
- proxy2model/ingest/crawl.py +673 -0
- proxy2model/ingest/extract.py +208 -0
- proxy2model/ingest/infer.py +75 -0
- proxy2model/ingest/journey.py +148 -0
- proxy2model/ingest/live.py +96 -0
- proxy2model/ingest/parse.py +1275 -0
- proxy2model/ingest/redact.py +535 -0
- proxy2model/py.typed +0 -0
- proxy2model/recon/__init__.py +1 -0
- proxy2model/recon/osint.py +607 -0
- proxy2model/report/__init__.py +1 -0
- proxy2model/report/animate.py +163 -0
- proxy2model/report/dash.py +469 -0
- proxy2model/report/fonts.py +51 -0
- proxy2model/report/html.py +988 -0
- proxy2model/report/inventory.py +258 -0
- proxy2model/report/map.py +515 -0
- proxy2model/report/pages.py +537 -0
- proxy2model/report/query.py +306 -0
- proxy2model/report/shot.py +55 -0
- proxy2model/report/view.py +213 -0
- proxy2model-2.0.0.dist-info/METADATA +148 -0
- proxy2model-2.0.0.dist-info/RECORD +66 -0
- proxy2model-2.0.0.dist-info/WHEEL +5 -0
- proxy2model-2.0.0.dist-info/entry_points.txt +2 -0
- proxy2model-2.0.0.dist-info/licenses/LICENSE +201 -0
- proxy2model-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""The Map view of the offline report: an interactive graph of the whole app on a canvas.
|
|
2
|
+
|
|
3
|
+
Three layouts (clustered force, layered flow, and a focus ring around one node), pan and zoom,
|
|
4
|
+
hover tracing, a node inspector, path tracing between two nodes, search, layer and edge filters
|
|
5
|
+
and a minimap. Positions are deterministic: the same model always draws the same picture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
MAP_CSS = r"""
|
|
9
|
+
/* map */
|
|
10
|
+
.m-graph{padding:0!important;overflow:hidden!important}
|
|
11
|
+
.m-graph .view{max-width:none;height:100%}
|
|
12
|
+
.m-graph .foot{display:none}
|
|
13
|
+
.mapx{display:flex;flex-direction:column;height:100%;outline:none}
|
|
14
|
+
.mtop{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
15
|
+
.mtop h1{font:700 15px var(--sans);margin:0 4px 0 0}
|
|
16
|
+
.msearch{position:relative;flex:0 1 250px;min-width:170px}
|
|
17
|
+
.msearch input{width:100%;height:32px;padding:0 10px 0 30px;border:1px solid var(--line2);border-radius:7px;background:var(--panel2);color:var(--text);font:12.5px var(--sans)}
|
|
18
|
+
.msearch input:focus{border-color:var(--signal);outline:none}
|
|
19
|
+
.msearch svg{position:absolute;left:9px;top:8px;width:15px;height:15px;color:var(--faint)}
|
|
20
|
+
.msug{position:absolute;left:0;right:0;top:36px;background:var(--panel);border:1px solid var(--line2);border-radius:9px;box-shadow:var(--shadow);z-index:40;max-height:260px;overflow:auto;padding:3px}
|
|
21
|
+
.msug button{display:flex;gap:8px;align-items:center;width:100%;text-align:left;padding:6px 8px;border:0;background:none;border-radius:6px;font:12px var(--mono);color:var(--text)}
|
|
22
|
+
.msug button:hover,.msug button.on{background:var(--signal-soft)}
|
|
23
|
+
.msug .k{font:600 9.5px var(--mono);color:var(--faint);margin-left:auto;text-transform:uppercase}
|
|
24
|
+
.mchips{display:flex;gap:5px;flex-wrap:wrap;align-items:center;flex-basis:100%;order:9}
|
|
25
|
+
.mchip{display:inline-flex;gap:6px;align-items:center;font:600 11px var(--mono);padding:4px 9px;border-radius:7px;border:1px solid var(--line2);background:var(--panel);color:var(--text)}
|
|
26
|
+
.mchip .sw{width:9px;height:9px;border-radius:2px;flex:none}
|
|
27
|
+
.mchip .c{color:var(--faint)}
|
|
28
|
+
.mchip.off{opacity:.42;text-decoration:line-through}
|
|
29
|
+
.mchip:hover{border-color:var(--faint)}
|
|
30
|
+
.mchips .div{width:1px;height:20px;background:var(--line2);margin:0 4px}
|
|
31
|
+
.mseg{display:inline-flex;border:1px solid var(--line2);border-radius:8px;overflow:hidden}
|
|
32
|
+
.mseg button{font:600 11.5px var(--sans);padding:5px 11px;border:0;background:var(--panel);color:var(--muted)}
|
|
33
|
+
.mseg button+button{border-left:1px solid var(--line2)}
|
|
34
|
+
.mseg button.on{background:var(--text);color:var(--ink)}
|
|
35
|
+
.mbtn{font:600 12px var(--sans);padding:5px 10px;border:1px solid var(--line2);border-radius:8px;background:var(--panel);color:var(--muted)}
|
|
36
|
+
.mbtn:hover{color:var(--text)}
|
|
37
|
+
.mbody{display:flex;flex:1;min-height:0}
|
|
38
|
+
.mcv{position:relative;flex:1;min-width:0;background:radial-gradient(1200px 760px at 50% 42%,#fbf8f2,#efe8dc)}
|
|
39
|
+
.mcv.dark{background:radial-gradient(1200px 760px at 50% 42%,#0d1118,#05070a)}
|
|
40
|
+
.mcv.dark .mhud,.mcv.dark canvas.mini,.mcv.dark .mzoom button{background:#100e0b;border-color:#24201a;color:#a39a8d}
|
|
41
|
+
.mcv.dark .mtip{background:#100e0b;border-color:#332d25;color:#f3ece0}
|
|
42
|
+
.mcv canvas.main{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:grab;touch-action:none}
|
|
43
|
+
.mcv canvas.main.drag{cursor:grabbing}.mcv canvas.main.hit{cursor:pointer}
|
|
44
|
+
.mcv canvas.mini{position:absolute;right:14px;bottom:14px;width:170px;height:110px;border:1px solid var(--line2);border-radius:10px;background:var(--panel);opacity:.94;cursor:pointer;box-shadow:var(--shadow)}
|
|
45
|
+
.mhud{position:absolute;left:14px;bottom:14px;color:var(--muted);font:11.5px var(--mono);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 10px;pointer-events:none}
|
|
46
|
+
.mtip{position:absolute;pointer-events:none;z-index:5;background:var(--panel);border:1px solid var(--line2);border-radius:9px;box-shadow:var(--shadow);padding:7px 10px;font:12px var(--mono);max-width:340px;display:none}
|
|
47
|
+
.mtip b{display:block;font:700 12.5px var(--mono);word-break:break-all}
|
|
48
|
+
.mtip span{color:var(--muted);font-size:11px}
|
|
49
|
+
.mzoom{position:absolute;right:14px;top:14px;display:flex;flex-direction:column;gap:6px}
|
|
50
|
+
.mzoom button{width:32px;height:32px;border:1px solid var(--line2);border-radius:8px;background:var(--panel);font:700 15px var(--sans);color:var(--muted)}
|
|
51
|
+
.mzoom button:hover{color:var(--text)}
|
|
52
|
+
.minsp{width:330px;flex:none;border-left:1px solid var(--line);background:var(--panel);overflow:auto;padding:16px 16px 30px}
|
|
53
|
+
@media(max-width:1000px){.minsp{display:none}}
|
|
54
|
+
.minsp h3{font:700 11px var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin:18px 0 8px}
|
|
55
|
+
.minsp h3:first-child{margin-top:0}
|
|
56
|
+
.nh{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
57
|
+
.nh .ic{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex:none;background:var(--panel2)}
|
|
58
|
+
.nh .ic svg{width:16px;height:16px}
|
|
59
|
+
.nh .kind{font:600 10.5px var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
|
|
60
|
+
.nlabel{font:700 14px/1.35 var(--mono);word-break:break-all;margin:2px 0 10px}
|
|
61
|
+
.nrows{display:grid;grid-template-columns:90px 1fr;gap:6px 10px;font-size:12.5px}
|
|
62
|
+
.nrows .k{color:var(--muted)}.nrows .v{word-break:break-word;font-family:var(--mono);font-size:12px}
|
|
63
|
+
.nact{display:flex;gap:6px;flex-wrap:wrap;margin:14px 0 4px}
|
|
64
|
+
.conn{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:7px;cursor:pointer;font:12px var(--mono);border:1px solid transparent}
|
|
65
|
+
.conn:hover{background:var(--panel2);border-color:var(--line)}
|
|
66
|
+
.conn .a{color:var(--faint);flex:none;width:14px}
|
|
67
|
+
.conn .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
|
|
68
|
+
.conn .e{font:600 9.5px var(--mono);color:var(--muted);flex:none}
|
|
69
|
+
.conn .s{width:8px;height:8px;border-radius:2px;flex:none}
|
|
70
|
+
.ghd{font:600 11px var(--mono);color:var(--muted);margin:10px 0 3px;display:flex;gap:6px;align-items:center}
|
|
71
|
+
.ghd .st{font-size:9.5px;border:1px solid currentColor;border-radius:4px;padding:0 4px}
|
|
72
|
+
.ghd .st.ob{color:var(--observed)}.ghd .st.in{color:var(--inferred)}.ghd .st.ex{color:var(--l6)}
|
|
73
|
+
.pathbox{border:1px solid var(--signal);background:var(--signal-soft);border-radius:10px;padding:10px 12px;margin-top:12px}
|
|
74
|
+
.pathbox .pt{font:700 11px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--signal);margin-bottom:6px}
|
|
75
|
+
.hubrow{display:flex;gap:8px;align-items:center;padding:6px 0;border-bottom:1px solid var(--line);cursor:pointer;font:12px var(--mono)}
|
|
76
|
+
.hubrow:hover .t{color:var(--signal)}
|
|
77
|
+
.hubrow .t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
78
|
+
.hubrow .d{color:var(--faint)}
|
|
79
|
+
.tips{color:var(--muted);font-size:12px;line-height:1.75}
|
|
80
|
+
.tips kbd{font:600 10.5px var(--mono);border:1px solid var(--line2);border-bottom-width:2px;border-radius:4px;padding:0 5px;background:var(--panel2);color:var(--text)}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
MAP_JS = r"""
|
|
84
|
+
/* ---------- MAP (canvas, deterministic layouts, inspector) ---------- */
|
|
85
|
+
const CAT=[
|
|
86
|
+
{id:"infra",label:"Infrastructure",short:"Infra",types:["infra"],color:"#fb923c",shape:"square"},
|
|
87
|
+
{id:"host",label:"Hosts",short:"Hosts",types:["host"],color:"#8fa8cc",shape:"rsquare"},
|
|
88
|
+
{id:"tech",label:"Technology",short:"Tech",types:["tech"],color:"#6ee7b7",shape:"hex"},
|
|
89
|
+
{id:"route",label:"Pages / routes",short:"Pages",types:["route"],color:"#34d399",shape:"circle"},
|
|
90
|
+
{id:"script",label:"Scripts",short:"Scripts",types:["script"],color:"#a78bfa",shape:"diamond"},
|
|
91
|
+
{id:"endpoint",label:"API endpoints",short:"Endpoints",types:["endpoint"],color:"#4aa3ff",shape:"circle"},
|
|
92
|
+
{id:"param",label:"Parameters",short:"Params",types:["parameter","operation"],color:"#c4b5fd",shape:"dot"},
|
|
93
|
+
{id:"trust",label:"Trust & third parties",short:"Trust",types:["third_party","auth","cookie","role"],color:"#fbbf24",shape:"tri"},
|
|
94
|
+
];
|
|
95
|
+
function catOf(t){for(let i=0;i<CAT.length;i++)if(CAT[i].types.includes(t))return i;return CAT.length-1;}
|
|
96
|
+
function shapePath(shape,r){
|
|
97
|
+
const s=r*1.15;
|
|
98
|
+
switch(shape){
|
|
99
|
+
case "square":return `M${-s} ${-s}H${s}V${s}H${-s}Z`;
|
|
100
|
+
case "rsquare":{const k=s*.45;return `M${-s+k} ${-s}H${s-k}Q${s} ${-s} ${s} ${-s+k}V${s-k}Q${s} ${s} ${s-k} ${s}H${-s+k}Q${-s} ${s} ${-s} ${s-k}V${-s+k}Q${-s} ${-s} ${-s+k} ${-s}Z`;}
|
|
101
|
+
case "diamond":return `M0 ${-s*1.2}L${s*1.2} 0L0 ${s*1.2}L${-s*1.2} 0Z`;
|
|
102
|
+
case "hex":return [0,1,2,3,4,5].map(i=>{const a=Math.PI/3*i;return (i?"L":"M")+(s*Math.cos(a)).toFixed(2)+" "+(s*Math.sin(a)).toFixed(2);}).join("")+"Z";
|
|
103
|
+
case "tri":return `M0 ${-s*1.15}L${s*1.1} ${s*.75}L${-s*1.1} ${s*.75}Z`;
|
|
104
|
+
case "dot":{const d=Math.max(3,r*.7);return `M${-d} 0a${d} ${d} 0 1 0 ${2*d} 0a${d} ${d} 0 1 0 ${-2*d} 0`;}
|
|
105
|
+
default:return `M${-r} 0a${r} ${r} 0 1 0 ${2*r} 0a${r} ${r} 0 1 0 ${-2*r} 0`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const ACCESS=new Set(["REACHED","SENT_CREDENTIAL"]);
|
|
109
|
+
/* every kind of node has its own colour and glyph, so the picture reads without the legend */
|
|
110
|
+
const TYPE_COL={host:"#8fa8cc",route:"#34d399",script:"#a78bfa",endpoint:"#4aa3ff",parameter:"#c4b5fd",operation:"#e879f9",third_party:"#22d3ee",auth:"#fbbf24",cookie:"#d6a85c",role:"#f472b6",tech:"#6ee7b7",infra:"#fb923c"};
|
|
111
|
+
/* deeper tones on a light canvas so white glyphs stay readable */
|
|
112
|
+
const TYPE_COL_LIGHT={host:"#5b7399",route:"#0f9d73",script:"#7c5cf0",endpoint:"#1d7fe0",parameter:"#8b7be0",operation:"#c026d3",third_party:"#0891b2",auth:"#d08400",cookie:"#a9742a",role:"#db2777",tech:"#16a34a",infra:"#ea580c"};
|
|
113
|
+
const mapDark=()=>document.documentElement.getAttribute("data-theme")==="dark";
|
|
114
|
+
const typeCol=t=>(mapDark()?TYPE_COL:TYPE_COL_LIGHT)[t]||"#94a3b8";
|
|
115
|
+
const MAPS=(function(){
|
|
116
|
+
const base={mode:"net",hidden:new Set((D.graph.nodes||[]).length>140?["param"]:[]),edgeOff:new Set((D.graph.edges||[]).length>300?["ACCESS"]:[]),sel:null,path:null,focusId:null};
|
|
117
|
+
try{const s=JSON.parse(localStorage.getItem("b2m-map2")||"null");if(s){base.mode=s.mode||base.mode;base.hidden=new Set(s.hidden||[...base.hidden]);base.edgeOff=new Set(s.edgeOff||[]);}}catch(e){}
|
|
118
|
+
return base;
|
|
119
|
+
})();
|
|
120
|
+
function mapSave(){try{localStorage.setItem("b2m-map2",JSON.stringify({mode:MAPS.mode,hidden:[...MAPS.hidden],edgeOff:[...MAPS.edgeOff]}));}catch(e){}}
|
|
121
|
+
const ICON={host:"M4 6h16v5H4zM4 13h16v5H4z",route:"M5 4h9l5 5v11H5z",script:"M8 7l-5 5 5 5M16 7l5 5-5 5",endpoint:"M4 12h6m4 0h6M10 8l4 4-4 4",
|
|
122
|
+
parameter:"M5 8h14M5 16h14",third_party:"M12 3l9 16H3z",auth:"M7 11V8a5 5 0 0 1 10 0v3M5 11h14v9H5z",cookie:"M12 3a9 9 0 1 0 9 9 4 4 0 0 1-4-4 4 4 0 0 1-5-5z",
|
|
123
|
+
role:"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21a8 8 0 0 1 16 0",tech:"M12 3l8 4.5v9L12 21l-8-4.5v-9z",infra:"M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z",operation:"M5 8h14M5 16h14"};
|
|
124
|
+
const KIND_LABEL={infra:"Infrastructure",host:"Host",tech:"Technology",route:"Page / route",script:"Script",endpoint:"API endpoint",parameter:"Parameter",operation:"GraphQL operation",third_party:"Third party",auth:"Auth scheme",cookie:"Cookie",role:"Role"};
|
|
125
|
+
|
|
126
|
+
const AREA_SKIP=new Set(["api","rest","v1","v2","v3","v4","graphql","gql","apis","public","internal","www"]);
|
|
127
|
+
function areaOf(n){
|
|
128
|
+
let p;
|
|
129
|
+
if(n.type==="endpoint"){const m=/^\S+\s+(.*)$/.exec(n.label);p=m?m[1]:n.label;}
|
|
130
|
+
else if(n.type==="route")p=n.label;else return null;
|
|
131
|
+
p=p.replace(/^[^/]*/,"");
|
|
132
|
+
const segs=p.split("?")[0].split("/").filter(x=>x&&!AREA_SKIP.has(x.toLowerCase())&&!/^\{.*\}$/.test(x)&&!/^[\d.]+$/.test(x)&&!/^[a-f0-9-]{16,}$/i.test(x));
|
|
133
|
+
const a=(segs[0]||"root").toLowerCase().replace(/[^a-z0-9_.-]/g,"").replace(/\.[a-z0-9]{1,5}$/,"")||"root";
|
|
134
|
+
return a.length>4&&a.endsWith("s")&&!a.endsWith("ss")?a.slice(0,-1):a;
|
|
135
|
+
}
|
|
136
|
+
function vGraph(){
|
|
137
|
+
const present=CAT.map((cat,i)=>({cat,i,n:(D.graph.nodes||[]).filter(x=>catOf(x.type)===i).length})).filter(x=>x.n);
|
|
138
|
+
const states=["OBSERVED","INFERRED","EXTERNAL"].filter(s=>(D.graph.edges||[]).some(e=>e.state===s)).concat((D.graph.edges||[]).some(e=>ACCESS.has(e.type))?["ACCESS"]:[]);
|
|
139
|
+
const SN={OBSERVED:"seen in traffic",INFERRED:"found in code",EXTERNAL:"recon",ACCESS:"who reached what"};
|
|
140
|
+
return `<div class="mapx" id="mapx" tabindex="-1"><div class="mtop"><h1>Map</h1>
|
|
141
|
+
<div class="msearch"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg><input id="msq" placeholder="Find a node…" autocomplete="off" spellcheck="false"><div class="msug" id="msug" hidden></div></div>
|
|
142
|
+
<span style="flex:1"></span>
|
|
143
|
+
<div class="mseg" id="mmode"><button data-mode="net" class="${MAPS.mode==="net"?"on":""}" title="Nodes and the links between them">Network</button><button data-mode="force" class="${MAPS.mode==="force"?"on":""}" title="Grouped by feature area">Cluster</button><button data-mode="flow" class="${MAPS.mode==="flow"?"on":""}" title="Layers left to right">Flow</button><button data-mode="focus" class="${MAPS.mode==="focus"?"on":""}" title="The selected node and what surrounds it">Focus</button></div>
|
|
144
|
+
<button class="mbtn" id="mpng" title="Download the map as a PNG">PNG</button><div class="mchips" id="mlayers">${present.map(x=>`<button class="mchip${MAPS.hidden.has(x.cat.id)?" off":""}" data-layer="${x.cat.id}" title="Show or hide"><span class="sw" style="background:${x.cat.color}"></span>${esc(x.cat.short)} <span class="c">${x.n}</span></button>`).join("")}
|
|
145
|
+
<span class="div"></span>${states.map(s=>`<button class="mchip${MAPS.edgeOff.has(s)?" off":""}" data-es="${s}" title="Show or hide these edges"><span class="sw" style="background:none;border-top:2px ${s==="INFERRED"?"dashed":s==="EXTERNAL"?"dotted":"solid"} ${s==="INFERRED"?"var(--inferred)":s==="EXTERNAL"?"var(--l6)":s==="ACCESS"?"var(--faint)":"var(--observed)"};border-radius:0;height:0;margin-top:1px"></span>${SN[s]}</button>`).join("")}</div></div>
|
|
146
|
+
<div class="mbody"><div class="mcv${mapDark()?" dark":""}" id="mcv"><canvas class="main" id="mc"></canvas><canvas class="mini" id="mm" width="340" height="220"></canvas><div class="mhud" id="mhud"></div><div class="mtip" id="mtip"></div>
|
|
147
|
+
<div class="mzoom"><button data-z="in" aria-label="Zoom in">+</button><button data-z="out" aria-label="Zoom out">−</button><button data-z="fit" aria-label="Fit" style="font-size:11px">Fit</button></div></div>
|
|
148
|
+
<aside class="minsp" id="minsp"></aside></div></div>`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function initGraph(){
|
|
152
|
+
const cv=$("#mc");if(!cv)return;
|
|
153
|
+
const wrap=$("#mcv"),ctx=cv.getContext("2d"),mini=$("#mm"),mctx=mini.getContext("2d"),tip=$("#mtip");
|
|
154
|
+
if(MAPS.raf)cancelAnimationFrame(MAPS.raf);
|
|
155
|
+
MAPS.sim=null;
|
|
156
|
+
/* ---- data ---- */
|
|
157
|
+
const all=D.graph.nodes.map(n=>({...n,cat:catOf(n.type),x:0,y:0,tx:0,ty:0,vx:0,vy:0,deg:0,fx:null,fy:null}));
|
|
158
|
+
const byId=new Map(all.map(n=>[n.id,n]));
|
|
159
|
+
const vis=n=>!MAPS.hidden.has(CAT[n.cat].id);
|
|
160
|
+
let nodes=all.filter(vis);
|
|
161
|
+
const nset=new Set(nodes.map(n=>n.id));
|
|
162
|
+
let edges=D.graph.edges.filter(e=>nset.has(e.s)&&nset.has(e.d)&&!MAPS.edgeOff.has(e.state)&&!(MAPS.edgeOff.has("ACCESS")&&ACCESS.has(e.type))).map(e=>({s:byId.get(e.s),d:byId.get(e.d),type:e.type,state:e.state}));
|
|
163
|
+
nodes.forEach(n=>n.deg=0);edges.forEach(e=>{e.s.deg++;e.d.deg++;});
|
|
164
|
+
const adj=new Map(nodes.map(n=>[n.id,[]]));edges.forEach(e=>{adj.get(e.s.id).push({n:e.d,e,out:true});adj.get(e.d.id).push({n:e.s,e,out:false});});
|
|
165
|
+
const EPBY=Object.fromEntries(EP.map(e=>[e.id,e]));
|
|
166
|
+
const BASE_R={host:14,route:10,script:11,endpoint:9,parameter:6,operation:7,third_party:10,auth:9,cookie:9,role:10,tech:9,infra:9};
|
|
167
|
+
const R=n=>(BASE_R[n.type]||9)+Math.min(6,Math.sqrt(n.deg)*.8);
|
|
168
|
+
const cs=getComputedStyle(document.documentElement);
|
|
169
|
+
const col=v=>{const m=/^var\((--[\w-]+)\)$/.exec(v);return m?cs.getPropertyValue(m[1]).trim():v;};
|
|
170
|
+
let C={},CV={};const readTheme=()=>{["text","muted","faint","line","line2","panel","panel2","ink","signal","observed","inferred","ok","warn","bad","l6"].forEach(k=>C[k]=cs.getPropertyValue("--"+k).trim());CAT.forEach(c=>c._c=col(c.color));
|
|
171
|
+
CV=mapDark()?{bg:"#0b0a08",edge:"#b5ab9c",lab:"#f3ece0",labm:"#a39a8d",halo:"#0b0a08",ghost:"#0b0a08",ring:"#ffffff"}:{bg:"#fbf8f2",edge:"#7a7064",lab:"#0b0a08",labm:"#5f584e",halo:"#fbf8f2",ghost:"#fbf8f2",ring:"#0b0a08"};if(wrap)wrap.classList.toggle("dark",mapDark());};
|
|
172
|
+
readTheme();
|
|
173
|
+
const themeObs=new MutationObserver(()=>{readTheme();dirty=true;});themeObs.observe(document.documentElement,{attributes:true,attributeFilter:["data-theme"]});
|
|
174
|
+
const ICONP={};const iconPath=t=>ICONP[t]||(ICONP[t]=new Path2D(ICON[t]||ICON.endpoint));
|
|
175
|
+
const pathCache={};const P2=(shape,r)=>{const k=shape+Math.round(r*2);return pathCache[k]||(pathCache[k]=new Path2D(shapePath(shape,Math.round(r*2)/2)));};
|
|
176
|
+
|
|
177
|
+
/* ---- layouts (all deterministic) ---- */
|
|
178
|
+
function layoutFlow(){
|
|
179
|
+
const cols=CAT.map((_,i)=>i).filter(i=>nodes.some(n=>n.cat===i)),ROWH=26,SUB=30,COLW=210;
|
|
180
|
+
const by={};cols.forEach(ci=>by[ci]=nodes.filter(n=>n.cat===ci).sort((a,b)=>String(a.label).localeCompare(String(b.label))));
|
|
181
|
+
const rank={};cols.forEach(ci=>by[ci].forEach((n,r)=>rank[n.id]=r));
|
|
182
|
+
for(let sw=0;sw<6;sw++){(sw%2?cols.slice().reverse():cols).forEach(ci=>{
|
|
183
|
+
const bary=n=>{const ns=adj.get(n.id).map(x=>rank[x.n.id]).filter(v=>v!=null);return ns.length?ns.reduce((a,b)=>a+b,0)/ns.length:rank[n.id];};
|
|
184
|
+
by[ci].sort((a,b)=>bary(a)-bary(b)||String(a.label).localeCompare(String(b.label)));by[ci].forEach((n,r)=>rank[n.id]=r);});}
|
|
185
|
+
let x=0;const maxRows=Math.max(1,...cols.map(ci=>Math.min(SUB,by[ci].length)));
|
|
186
|
+
cols.forEach(ci=>{const nsub=Math.max(1,Math.ceil(by[ci].length/SUB)),off=(maxRows-Math.min(SUB,by[ci].length))*ROWH/2;
|
|
187
|
+
by[ci].forEach((n,r)=>{n.tx=x+Math.floor(r/SUB)*COLW;n.ty=off+(r%SUB)*ROWH;});x+=nsub*COLW+60;});
|
|
188
|
+
}
|
|
189
|
+
function layoutForce(net){
|
|
190
|
+
const SP=net?1.1:1.5;
|
|
191
|
+
/* every (kind, feature area) is a bubble. Bubbles are packed without overlap, grouped by kind around a ring,
|
|
192
|
+
so the picture reads as neighbourhoods. Nodes then relax inside their bubble. */
|
|
193
|
+
const groups=new Map();
|
|
194
|
+
nodes.forEach(n=>{n.area=areaOf(n);const k=n.cat+"|"+(n.area||"");(groups.get(k)||groups.set(k,{cat:n.cat,name:n.area||CAT[n.cat].short,g:[]}).get(k)).g.push(n);});
|
|
195
|
+
const bub=[...groups.values()].sort((x,y)=>x.cat-y.cat||y.g.length-x.g.length||x.name.localeCompare(y.name));
|
|
196
|
+
bub.forEach(b=>{b.r=(16+9.5*Math.sqrt(b.g.length))*SP;});
|
|
197
|
+
const regions=[];
|
|
198
|
+
CAT.forEach((c,ci)=>{const bs=bub.filter(b=>b.cat===ci);if(!bs.length)return;
|
|
199
|
+
const placed=[];
|
|
200
|
+
bs.forEach((b,i)=>{
|
|
201
|
+
if(!i){b.x=0;b.y=0;placed.push(b);return;}
|
|
202
|
+
for(let rad=0,ang=0;;ang+=.55,rad+=.9){const x=Math.cos(ang)*rad*3.2,y=Math.sin(ang)*rad*3.2;
|
|
203
|
+
if(placed.every(q=>Math.hypot(q.x-x,q.y-y)>=q.r+b.r+34*SP)){b.x=x;b.y=y;placed.push(b);break;}if(rad>4000){b.x=rad;b.y=0;placed.push(b);break;}}});
|
|
204
|
+
let mx=0,my=0;placed.forEach(b=>{mx+=b.x*b.g.length;my+=b.y*b.g.length;});const tot=placed.reduce((a,b)=>a+b.g.length,0);mx/=tot;my/=tot;
|
|
205
|
+
placed.forEach(b=>{b.x-=mx;b.y-=my;});
|
|
206
|
+
regions.push({ci,bs:placed,R:Math.max(...placed.map(b=>Math.hypot(b.x,b.y)+b.r))});});
|
|
207
|
+
/* regions along a ring, spaced by their own size */
|
|
208
|
+
const need=regions.reduce((a,r)=>a+2*r.R+90*SP,0),RING=Math.max(regions.length>1?need/(2*Math.PI):0,regions.length>1?Math.max(...regions.map(r=>r.R))+80*SP:0);
|
|
209
|
+
let ang=-Math.PI/2;
|
|
210
|
+
regions.forEach(r=>{const share=(2*r.R+90*SP)/Math.max(1,need)*2*Math.PI;ang+=share/2;
|
|
211
|
+
const cx=regions.length>1?Math.cos(ang)*RING:0,cy=regions.length>1?Math.sin(ang)*RING*.82:0;ang+=share/2;
|
|
212
|
+
r.bs.forEach(b=>{b.ax=cx+b.x;b.ay=cy+b.y;b.g.forEach(n=>{n.ax=b.ax;n.ay=b.ay;});});});
|
|
213
|
+
MAPS.clusters=bub.filter(b=>b.g.length>=2||b.g[0].cat===1).map(b=>({g:b.g,name:b.name,cat:b.cat,r:b.r}));
|
|
214
|
+
const cnt={};
|
|
215
|
+
nodes.slice().sort((a,b)=>a.cat-b.cat||String(a.label).localeCompare(String(b.label))).forEach(n=>{
|
|
216
|
+
const k=n.ax+","+n.ay,i=cnt[k]=(cnt[k]||0)+1,a=i*2.399963,r=9*SP*Math.sqrt(i);
|
|
217
|
+
n.x=n.ax+Math.cos(a)*r;n.y=n.ay+Math.sin(a)*r;n.vx=n.vy=0;});
|
|
218
|
+
const sim=net?{net:true,alpha:1}:{anchor:{},alpha:1};MAPS.sim=sim;
|
|
219
|
+
const steps=nodes.length>1500?60:nodes.length>600?140:net?300:240;
|
|
220
|
+
for(let i=0;i<steps;i++)(net?tickNet:tick)(sim,true);
|
|
221
|
+
nodes.forEach(n=>{n.tx=n.x;n.ty=n.y;});
|
|
222
|
+
sim.alpha=0;
|
|
223
|
+
}
|
|
224
|
+
function tick(sim,pre){
|
|
225
|
+
const a=sim.alpha,N=nodes,CELL=60,grid=new Map();
|
|
226
|
+
N.forEach(n=>{const k=Math.floor(n.x/CELL)+","+Math.floor(n.y/CELL);(grid.get(k)||grid.set(k,[]).get(k)).push(n);});
|
|
227
|
+
N.forEach(n=>{const cx=Math.floor(n.x/CELL),cy=Math.floor(n.y/CELL);
|
|
228
|
+
for(let gx=cx-2;gx<=cx+2;gx++)for(let gy=cy-2;gy<=cy+2;gy++){const g=grid.get(gx+","+gy);if(!g)continue;
|
|
229
|
+
for(const m of g){if(m===n||m.id<n.id)continue;let dx=m.x-n.x,dy=m.y-n.y,d2=dx*dx+dy*dy;if(d2<.01){dx=(n.deg%7-3)*.1+.05;dy=(m.deg%5-2)*.1+.05;d2=dx*dx+dy*dy;}
|
|
230
|
+
if(d2>120*120)continue;const d=Math.sqrt(Math.max(d2,36)),f=Math.min(30,700/(d*d)+(d<R(n)+R(m)+5?3:0))*a,fx=dx/Math.sqrt(d2)*f,fy=dy/Math.sqrt(d2)*f;n.vx-=fx;n.vy-=fy;m.vx+=fx;m.vy+=fy;}}});
|
|
231
|
+
edges.forEach(e=>{if(ACCESS.has(e.type)||e.s.deg>24||e.d.deg>24||e.s.ax!==e.d.ax||e.s.ay!==e.d.ay)return;const dx=e.d.x-e.s.x,dy=e.d.y-e.s.y,d=Math.max(1,Math.sqrt(dx*dx+dy*dy)),rest=e.type==="USES_PARAMETER"?30:60,f=(d-rest)/d*.03*a/Math.sqrt(Math.max(1,Math.max(e.s.deg,e.d.deg)));
|
|
232
|
+
e.s.vx+=dx*f;e.s.vy+=dy*f;e.d.vx-=dx*f;e.d.vy-=dy*f;});
|
|
233
|
+
N.forEach(n=>{n.vx+=(n.ax-n.x)*.09*a;n.vy+=(n.ay-n.y)*.09*a;
|
|
234
|
+
if(n.fx!=null){n.x=n.fx;n.y=n.fy;n.vx=n.vy=0;return;}
|
|
235
|
+
const sp=Math.hypot(n.vx,n.vy);if(sp>24){n.vx*=24/sp;n.vy*=24/sp;}
|
|
236
|
+
n.x+=n.vx;n.y+=n.vy;n.vx*=.58;n.vy*=.58;if(!isFinite(n.x)||!isFinite(n.y)){n.x=n.ax;n.y=n.ay;n.vx=n.vy=0;}});
|
|
237
|
+
sim.alpha=pre?a*.985:a*.965;
|
|
238
|
+
}
|
|
239
|
+
function layoutNet(){layoutForce(true);}
|
|
240
|
+
function tickNet(sim,pre){
|
|
241
|
+
const a=sim.alpha,N=nodes,CELL=150,grid=new Map();
|
|
242
|
+
N.forEach(n=>{const k=Math.floor(n.x/CELL)+","+Math.floor(n.y/CELL);(grid.get(k)||grid.set(k,[]).get(k)).push(n);});
|
|
243
|
+
N.forEach(n=>{const cx=Math.floor(n.x/CELL),cy=Math.floor(n.y/CELL);
|
|
244
|
+
for(let gx=cx-2;gx<=cx+2;gx++)for(let gy=cy-2;gy<=cy+2;gy++){const g=grid.get(gx+","+gy);if(!g)continue;
|
|
245
|
+
for(const m of g){if(m===n||m.id<n.id)continue;let dx=m.x-n.x,dy=m.y-n.y,d2=dx*dx+dy*dy;if(d2<.01){dx=(n.deg%7-3)*.1+.05;dy=(m.deg%5-2)*.1+.05;d2=dx*dx+dy*dy;}
|
|
246
|
+
if(d2>340*340)continue;const d=Math.sqrt(Math.max(d2,49)),f=(Math.min(45,6000/(d*d))+(d<R(n)+R(m)+26?6:0))*a,q=Math.sqrt(d2),fx=dx/q*f,fy=dy/q*f;n.vx-=fx;n.vy-=fy;m.vx+=fx;m.vy+=fy;}}});
|
|
247
|
+
edges.forEach(e=>{const dx=e.d.x-e.s.x,dy=e.d.y-e.s.y,d=Math.max(1,Math.sqrt(dx*dx+dy*dy)),hub=ACCESS.has(e.type)||Math.max(e.s.deg,e.d.deg)>24;
|
|
248
|
+
const rest=hub?240:e.type==="USES_PARAMETER"?60:125,k=hub?(ACCESS.has(e.type)?.004:0):.06,f=(d-rest)/d*k*a;
|
|
249
|
+
e.s.vx+=dx*f;e.s.vy+=dy*f;e.d.vx-=dx*f;e.d.vy-=dy*f;});
|
|
250
|
+
N.forEach(n=>{if(n.ax!=null){n.vx+=(n.ax-n.x)*.014*a;n.vy+=(n.ay-n.y)*.014*a;}else{n.vx-=n.x*.0025*a;n.vy-=n.y*.0025*a;}
|
|
251
|
+
if(n.fx!=null){n.x=n.fx;n.y=n.fy;n.vx=n.vy=0;return;}
|
|
252
|
+
const sp=Math.hypot(n.vx,n.vy);if(sp>30){n.vx*=30/sp;n.vy*=30/sp;}
|
|
253
|
+
n.x+=n.vx;n.y+=n.vy;n.vx*=.6;n.vy*=.6;if(!isFinite(n.x)||!isFinite(n.y)){n.x=n.y=0;n.vx=n.vy=0;}});
|
|
254
|
+
sim.alpha=pre?a*.988:a*.965;
|
|
255
|
+
}
|
|
256
|
+
function layoutFocus(){
|
|
257
|
+
const c=byId.get(MAPS.focusId||MAPS.sel)&&nset.has(MAPS.focusId||MAPS.sel)?byId.get(MAPS.focusId||MAPS.sel):nodes.filter(n=>(n.cat===3||n.cat===5)&&n.deg<60).sort((a,b)=>b.deg-a.deg||String(a.label).localeCompare(String(b.label)))[0]||nodes.slice().sort((a,b)=>b.deg-a.deg)[0];
|
|
258
|
+
if(!c)return;MAPS.focusId=c.id;
|
|
259
|
+
const dist=new Map([[c.id,0]]),q=[c];
|
|
260
|
+
while(q.length){const n=q.shift(),d=dist.get(n.id);if(d>=3)continue;adj.get(n.id).forEach(x=>{if(!dist.has(x.n.id)){dist.set(x.n.id,d+1);q.push(x.n);}});}
|
|
261
|
+
const rings=[[],[],[],[]];dist.forEach((d,id)=>rings[d].push(byId.get(id)));
|
|
262
|
+
nodes.forEach(n=>{n.focusOut=!dist.has(n.id);});
|
|
263
|
+
rings.forEach((rg,d)=>{if(!d){c.tx=0;c.ty=0;return;}
|
|
264
|
+
rg.sort((a,b)=>a.cat-b.cat||String(a.label).localeCompare(String(b.label)));
|
|
265
|
+
const rad=Math.max(130*d,rg.length*(d===1?15:11)/(2*Math.PI)+60*d);
|
|
266
|
+
rg.forEach((n,i)=>{const a=-Math.PI/2+i*2*Math.PI/rg.length;n.tx=Math.cos(a)*rad;n.ty=Math.sin(a)*rad;});});
|
|
267
|
+
/* everything beyond three hops parks far out, faded */
|
|
268
|
+
nodes.filter(n=>n.focusOut).forEach((n,i,arr)=>{const a=i*2*Math.PI/arr.length,r=520+Math.sqrt(arr.length)*12;n.tx=Math.cos(a)*r;n.ty=Math.sin(a)*r;});
|
|
269
|
+
}
|
|
270
|
+
function layout(){
|
|
271
|
+
nodes.forEach(n=>{n.focusOut=false;});
|
|
272
|
+
if(MAPS.mode==="flow")layoutFlow();else if(MAPS.mode==="focus")layoutFocus();else if(MAPS.mode==="force")layoutForce();else layoutNet();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ---- camera ---- */
|
|
276
|
+
let W=1,H=1,dpr=window.devicePixelRatio||1,T={x:0,y:0,k:1},dirty=true,anim=null;
|
|
277
|
+
/* nodes keep a readable size when you zoom out, so the icons never shrink to dots */
|
|
278
|
+
const RS=n=>Math.max(R(n)*T.k,nodes.length>300?6:9);
|
|
279
|
+
const resize=()=>{const r=wrap.getBoundingClientRect();W=Math.max(200,r.width);H=Math.max(200,r.height);dpr=window.devicePixelRatio||1;cv.width=W*dpr;cv.height=H*dpr;dirty=true;};
|
|
280
|
+
const bounds=()=>{let a=1e9,b=1e9,c2=-1e9,d=-1e9;const list=MAPS.mode==="focus"?nodes.filter(n=>!n.focusOut):nodes;list.forEach(n=>{a=Math.min(a,n.tx);b=Math.min(b,n.ty);c2=Math.max(c2,n.tx);d=Math.max(d,n.ty);});return list.length?{x0:a,y0:b,x1:c2,y1:d}:{x0:-100,y0:-100,x1:100,y1:100};};
|
|
281
|
+
function fit(animate){
|
|
282
|
+
const b=bounds(),pad=80,sw=Math.max(1,b.x1-b.x0),sh=Math.max(1,b.y1-b.y0);
|
|
283
|
+
const k=Math.max(.05,Math.min(2.2,Math.min((W-pad*2)/sw,(H-pad*2)/sh)));
|
|
284
|
+
const to={k,x:W/2-(b.x0+sw/2)*k,y:H/2-(b.y0+sh/2)*k};
|
|
285
|
+
if(animate){anim={from:{...T},to,t0:performance.now(),dur:420};}else{T=to;}dirty=true;
|
|
286
|
+
}
|
|
287
|
+
const toWorld=(px,py)=>({x:(px-T.x)/T.k,y:(py-T.y)/T.k});
|
|
288
|
+
function zoomAt(f,px,py){const k=Math.max(.05,Math.min(6,T.k*f)),w=toWorld(px,py);T={k,x:px-w.x*k,y:py-w.y*k};dirty=true;}
|
|
289
|
+
function flyTo(n){const k=Math.max(T.k,1.2);anim={from:{...T},to:{k,x:W/2-n.tx*k,y:H/2-n.ty*k},t0:performance.now(),dur:480};dirty=true;}
|
|
290
|
+
|
|
291
|
+
/* ---- highlight state ---- */
|
|
292
|
+
let hover=null,hl=null,pathSet=null,pathEdges=null,dash=0;
|
|
293
|
+
function nbrSet(id){const s=new Set([id]);(adj.get(id)||[]).forEach(x=>s.add(x.n.id));return s;}
|
|
294
|
+
function setPath(a,b){
|
|
295
|
+
if(!a||!b||a===b){MAPS.path=null;return;}
|
|
296
|
+
const prev=new Map([[a,null]]),q=[a];
|
|
297
|
+
while(q.length&&!prev.has(b)){const id=q.shift();(adj.get(id)||[]).forEach(x=>{if(!prev.has(x.n.id)){prev.set(x.n.id,id);q.push(x.n.id);}});}
|
|
298
|
+
if(!prev.has(b)){MAPS.path={from:a,to:b,ids:null};return;}
|
|
299
|
+
const ids=[];for(let c=b;c!=null;c=prev.get(c))ids.push(c);ids.reverse();MAPS.path={from:a,to:b,ids};
|
|
300
|
+
}
|
|
301
|
+
function recompute(){
|
|
302
|
+
const q=((($("#msq")||{}).value)||"").trim().toLowerCase();
|
|
303
|
+
hl=q?new Set(nodes.filter(n=>String(n.label).toLowerCase().includes(q)).map(n=>n.id)):null;
|
|
304
|
+
pathSet=null;pathEdges=null;
|
|
305
|
+
if(MAPS.path&&MAPS.path.ids){pathSet=new Set(MAPS.path.ids);pathEdges=new Set();for(let i=0;i<MAPS.path.ids.length-1;i++){pathEdges.add(MAPS.path.ids[i]+">"+MAPS.path.ids[i+1]);pathEdges.add(MAPS.path.ids[i+1]+">"+MAPS.path.ids[i]);}}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ---- drawing ---- */
|
|
309
|
+
function draw(now){
|
|
310
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);
|
|
311
|
+
const focusId=hover||MAPS.sel,fs=pathSet||(focusId?nbrSet(focusId):null);
|
|
312
|
+
const dimmed=(id)=>(hl&&!hl.has(id))||(fs&&!fs.has(id)&&!hl)||(MAPS.mode==="focus"&&byId.get(id).focusOut&&!hl);
|
|
313
|
+
const sx=n=>n.x*T.k+T.x,sy=n=>n.y*T.k+T.y;
|
|
314
|
+
/* edges: thin lines with arrowheads; the busiest hubs' links fade so the rest can be read */
|
|
315
|
+
const vx0=-40,vy0=-40,vx1=W+40,vy1=H+40,labelEdges=[];
|
|
316
|
+
ctx.lineCap="round";
|
|
317
|
+
for(const pass of [0,1]){
|
|
318
|
+
for(const e of edges){
|
|
319
|
+
const hi=pathEdges?pathEdges.has(e.s.id+">"+e.d.id):(focusId&&(e.s.id===focusId||e.d.id===focusId));
|
|
320
|
+
if((pass===1)!==!!hi)continue;
|
|
321
|
+
const ax=sx(e.s),ay=sy(e.s),bx=sx(e.d),by=sy(e.d);
|
|
322
|
+
if((ax<vx0&&bx<vx0)||(ax>vx1&&bx>vx1)||(ay<vy0&&by<vy0)||(ay>vy1&&by>vy1))continue;
|
|
323
|
+
const dim=!hi&&(dimmed(e.s.id)||dimmed(e.d.id));
|
|
324
|
+
const base=e.state==="INFERRED"?"#a78bfa":e.state==="EXTERNAL"?"#fb923c":CV.edge;
|
|
325
|
+
const hubby=Math.pow(Math.min(1,3.2/Math.sqrt(Math.max(e.s.deg,e.d.deg))),1.4);
|
|
326
|
+
ctx.globalAlpha=hi?1:dim?.04:Math.max(.05,(MAPS.mode==="net"?.62:.4)*hubby);
|
|
327
|
+
ctx.strokeStyle=hi?(pathEdges?C.signal:base):base;ctx.fillStyle=ctx.strokeStyle;ctx.lineWidth=hi?(pathEdges?2.6:1.7):1;
|
|
328
|
+
ctx.setLineDash(e.state==="INFERRED"?[5,4]:e.state==="EXTERNAL"?[1.5,4]:hi&&pathEdges?[7,5]:[]);
|
|
329
|
+
ctx.lineDashOffset=hi&&pathEdges?-dash:0;
|
|
330
|
+
const rs=RS(e.s),rd=RS(e.d);
|
|
331
|
+
ctx.beginPath();
|
|
332
|
+
let ex=bx,ey=by,ang;
|
|
333
|
+
if(MAPS.mode==="flow"){const sg=bx>=ax?1:-1,dx=Math.max(30,Math.abs(bx-ax)*.4);ctx.moveTo(ax,ay);ctx.bezierCurveTo(ax+sg*dx,ay,bx-sg*dx,by,bx,by);ang=Math.atan2(by-ay,bx-ax);ex=bx-Math.cos(ang)*rd;ey=by-Math.sin(ang)*rd;}
|
|
334
|
+
else{ang=Math.atan2(by-ay,bx-ax);const len=Math.hypot(bx-ax,by-ay);
|
|
335
|
+
if(len<=rs+rd+2){continue;}
|
|
336
|
+
const x0=ax+Math.cos(ang)*rs,y0=ay+Math.sin(ang)*rs;ex=bx-Math.cos(ang)*(rd+1.5);ey=by-Math.sin(ang)*(rd+1.5);
|
|
337
|
+
ctx.moveTo(x0,y0);ctx.lineTo(ex,ey);}
|
|
338
|
+
ctx.stroke();
|
|
339
|
+
if(Math.hypot(bx-ax,by-ay)>rs+rd+14&&(hi||T.k>.45)){const hs=hi?9:6.5;ctx.setLineDash([]);ctx.beginPath();ctx.moveTo(ex,ey);ctx.lineTo(ex-Math.cos(ang-.38)*hs,ey-Math.sin(ang-.38)*hs);ctx.lineTo(ex-Math.cos(ang+.38)*hs,ey-Math.sin(ang+.38)*hs);ctx.closePath();ctx.fill();}
|
|
340
|
+
if(hi||(T.k>=1.7&&hubby>.55&&Math.hypot(bx-ax,by-ay)>150))labelEdges.push({ax,ay,bx,by,t:humanize(e.type).toLowerCase(),hi,dim});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
ctx.setLineDash([]);ctx.globalAlpha=1;
|
|
344
|
+
/* neighbourhoods: a soft bubble and a name behind each group */
|
|
345
|
+
if(MAPS.mode==="force"&&MAPS.clusters&&!hl){
|
|
346
|
+
ctx.textAlign="center";ctx.textBaseline="middle";const lb=[];
|
|
347
|
+
for(const c of MAPS.clusters.slice().sort((x,y)=>y.g.length-x.g.length)){let mx=0,my=0;for(const n of c.g){mx+=n.x;my+=n.y;}mx/=c.g.length;my/=c.g.length;
|
|
348
|
+
let rad=0;for(const n of c.g)rad=Math.max(rad,Math.hypot(n.x-mx,n.y-my)+R(n)+10);
|
|
349
|
+
const cx=mx*T.k+T.x,cy=my*T.k+T.y,rr=rad*T.k;
|
|
350
|
+
if(cx+rr<0||cy+rr<0||cx-rr>W||cy-rr>H)continue;
|
|
351
|
+
if(c.g.length>=2){ctx.globalAlpha=fs?.5:1;ctx.beginPath();ctx.arc(cx,cy,rr,0,7);ctx.fillStyle=CAT[c.cat]._c;ctx.globalAlpha=(fs?.03:.075);ctx.fill();ctx.globalAlpha=fs?.12:.28;ctx.lineWidth=1;ctx.strokeStyle=CAT[c.cat]._c;ctx.setLineDash([2,5]);ctx.stroke();ctx.setLineDash([]);}
|
|
352
|
+
const fs2=Math.max(10,Math.min(18,9+rr/16));
|
|
353
|
+
const txt=c.name.toUpperCase()+(c.g.length>1?" "+c.g.length:"");ctx.font=`700 ${fs2}px Inter,system-ui,sans-serif`;
|
|
354
|
+
const w=ctx.measureText(txt).width,bx=[cx-w/2-3,cy-rr-fs2*.9-fs2/2,cx+w/2+3,cy-rr-fs2*.9+fs2/2];
|
|
355
|
+
if(lb.some(q=>!(bx[2]<q[0]||bx[0]>q[2]||bx[3]<q[1]||bx[1]>q[3])))continue;lb.push(bx);
|
|
356
|
+
ctx.globalAlpha=fs?.14:.75;ctx.fillStyle=CAT[c.cat]._c;ctx.fillText(txt,cx,cy-rr-fs2*.9);}
|
|
357
|
+
ctx.textAlign="start";ctx.globalAlpha=1;
|
|
358
|
+
}
|
|
359
|
+
/* nodes */
|
|
360
|
+
const order=nodes.slice().sort((a,b)=>(dimmed(a.id)?0:1)-(dimmed(b.id)?0:1)||a.deg-b.deg);
|
|
361
|
+
const labels=[];
|
|
362
|
+
for(const n of order){
|
|
363
|
+
const x=sx(n),y=sy(n);if(x<-30||y<-30||x>W+30||y>H+30)continue;
|
|
364
|
+
const r=RS(n),dim=dimmed(n.id),isSel=MAPS.sel===n.id,isHov=hover===n.id,match=hl&&hl.has(n.id);
|
|
365
|
+
const cat=CAT[n.cat],ep=EPBY[n.id],ghost=ep&&ep.state==="STATIC_ONLY",colr=typeCol(n.type);
|
|
366
|
+
ctx.globalAlpha=dim?.16:1;
|
|
367
|
+
ctx.save();ctx.translate(x,y);
|
|
368
|
+
if(isSel||isHov||match){ctx.beginPath();ctx.arc(0,0,r+7,0,7);ctx.fillStyle=colr;ctx.globalAlpha=(dim?.16:1)*.25;ctx.fill();ctx.globalAlpha=dim?.16:1;}
|
|
369
|
+
ctx.beginPath();ctx.arc(0,0,Math.max(2.5,r),0,7);
|
|
370
|
+
if(ghost){ctx.fillStyle=CV.ghost;ctx.fill();ctx.setLineDash([3.5,3]);ctx.lineWidth=2;ctx.strokeStyle=C.signal;ctx.stroke();ctx.setLineDash([]);}
|
|
371
|
+
else{ctx.fillStyle=colr;ctx.fill();if(isSel||match){ctx.lineWidth=3;ctx.strokeStyle=isSel?CV.ring:C.signal;ctx.stroke();}}
|
|
372
|
+
if(r>=6.5){const s=r*1.12/12;ctx.save();ctx.scale(s,s);ctx.translate(-12,-12);ctx.lineWidth=2.1/ (s>1?1:1);ctx.lineCap="round";ctx.lineJoin="round";ctx.strokeStyle=ghost?colr:"#ffffff";ctx.stroke(iconPath(n.type));ctx.restore();}
|
|
373
|
+
if(ep&&ep.privileged&&!ghost){ctx.beginPath();ctx.arc(r*.75,-r*.75,Math.max(2.6,r*.28),0,7);ctx.fillStyle=C.signal;ctx.fill();ctx.lineWidth=1.5;ctx.strokeStyle=CV.bg;ctx.stroke();}
|
|
374
|
+
ctx.restore();
|
|
375
|
+
if(!dim||match)labels.push({n,x,y,r,pri:(isSel?1e6:isHov?9e5:match?8e5:fs&&fs.has(n.id)?5e5:0)+n.deg});
|
|
376
|
+
}
|
|
377
|
+
ctx.globalAlpha=1;
|
|
378
|
+
/* relationship names along the edges */
|
|
379
|
+
ctx.font="10px Inter,system-ui,sans-serif";ctx.textAlign="center";ctx.textBaseline="middle";
|
|
380
|
+
for(const L of labelEdges){if(L.dim)continue;const mx=(L.ax+L.bx)/2,my=(L.ay+L.by)/2;let a=Math.atan2(L.by-L.ay,L.bx-L.ax);if(a>Math.PI/2||a<-Math.PI/2)a+=Math.PI;
|
|
381
|
+
ctx.save();ctx.translate(mx,my);ctx.rotate(a);ctx.lineWidth=3;ctx.strokeStyle=CV.halo;ctx.lineJoin="round";ctx.strokeText(L.t,0,-6);ctx.fillStyle=L.hi?CV.lab:CV.labm;ctx.globalAlpha=L.hi?1:.8;ctx.fillText(L.t,0,-6);ctx.restore();}
|
|
382
|
+
ctx.textAlign="start";ctx.globalAlpha=1;
|
|
383
|
+
/* labels: busiest first, never overlapping */
|
|
384
|
+
labels.sort((a,b)=>b.pri-a.pri);
|
|
385
|
+
const placed=[],showAll=T.k>=(MAPS.mode==="flow"?.42:MAPS.mode==="net"?.8:1.05),hubCut=Math.max(4,nodes.map(n=>n.deg).sort((a,b)=>b-a)[Math.floor(nodes.length*.08)]||4);
|
|
386
|
+
ctx.font=`${T.k>2?12:11}px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace`;ctx.textBaseline="middle";
|
|
387
|
+
for(const L of labels){
|
|
388
|
+
const force=L.pri>=1e5;if(!force&&!showAll&&L.n.deg<hubCut)continue;
|
|
389
|
+
let t=String(L.n.label||"");if(t.length>(force?60:30))t=t.slice(0,(force?59:29))+"…";
|
|
390
|
+
const w=ctx.measureText(t).width,x0=L.x-w/2,ly=L.y+L.r+11,y0=ly-7,box=[x0-2,y0,x0+w+2,y0+14];
|
|
391
|
+
if(!force&&placed.some(b=>!(box[2]<b[0]||box[0]>b[2]||box[3]<b[1]||box[1]>b[3])))continue;
|
|
392
|
+
placed.push(box);
|
|
393
|
+
ctx.lineWidth=3.5;ctx.strokeStyle=CV.halo;ctx.lineJoin="round";ctx.strokeText(t,x0,ly);
|
|
394
|
+
ctx.fillStyle=L.pri>=5e5?CV.lab:CV.labm;ctx.fillText(t,x0,ly);
|
|
395
|
+
}
|
|
396
|
+
$("#mhud").textContent=`${nodes.length} nodes · ${edges.length} edges · ${Math.round(T.k*100)}%`;
|
|
397
|
+
drawMini();
|
|
398
|
+
}
|
|
399
|
+
function drawMini(){
|
|
400
|
+
const mw=mini.width,mh=mini.height;mctx.clearRect(0,0,mw,mh);
|
|
401
|
+
const b=bounds(),pad=14,sw=Math.max(1,b.x1-b.x0),sh=Math.max(1,b.y1-b.y0),k=Math.min((mw-pad*2)/sw,(mh-pad*2)/sh);
|
|
402
|
+
const ox=(mw-sw*k)/2-b.x0*k,oy=(mh-sh*k)/2-b.y0*k;mini._m={k,ox,oy};
|
|
403
|
+
nodes.forEach(n=>{mctx.fillStyle=typeCol(n.type);mctx.globalAlpha=.85;mctx.fillRect(n.tx*k+ox-1.5,n.ty*k+oy-1.5,3,3);});
|
|
404
|
+
mctx.globalAlpha=1;
|
|
405
|
+
const a=toWorld(0,0),c2=toWorld(W,H);
|
|
406
|
+
mctx.strokeStyle=C.signal;mctx.lineWidth=2;mctx.strokeRect(a.x*k+ox,a.y*k+oy,(c2.x-a.x)*k,(c2.y-a.y)*k);
|
|
407
|
+
}
|
|
408
|
+
/* ---- frame loop: only runs while something moves ---- */
|
|
409
|
+
function frame(now){
|
|
410
|
+
if(!document.body.contains(cv)){themeObs.disconnect();return;}
|
|
411
|
+
let moving=false;
|
|
412
|
+
if(anim){const t=Math.min(1,(now-anim.t0)/anim.dur),e=1-Math.pow(1-t,3);T={k:anim.from.k+(anim.to.k-anim.from.k)*e,x:anim.from.x+(anim.to.x-anim.from.x)*e,y:anim.from.y+(anim.to.y-anim.from.y)*e};if(t>=1)anim=null;moving=true;}
|
|
413
|
+
if(MAPS.sim&&MAPS.sim.alpha>.02){(MAPS.sim.net?tickNet:tick)(MAPS.sim,false);nodes.forEach(n=>{n.tx=n.x;n.ty=n.y;});moving=true;}
|
|
414
|
+
else for(const n of nodes){if(n.fx!=null)continue;const dx=n.tx-n.x,dy=n.ty-n.y;if(Math.abs(dx)>.15||Math.abs(dy)>.15){n.x+=dx*.22;n.y+=dy*.22;moving=true;}else{n.x=n.tx;n.y=n.ty;}}
|
|
415
|
+
const animating=!!(pathEdges||MAPS.sel||hover);
|
|
416
|
+
if(animating){dash=(dash+.35)%24;}
|
|
417
|
+
if(dirty||moving||animating){draw(now);dirty=false;}
|
|
418
|
+
MAPS.raf=requestAnimationFrame(frame);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ---- picking and interaction ---- */
|
|
422
|
+
function pick(px,py){
|
|
423
|
+
let best=null,bd=1e9;
|
|
424
|
+
for(const n of nodes){if(MAPS.mode==="focus"&&n.focusOut)continue;const dx=n.x*T.k+T.x-px,dy=n.y*T.k+T.y-py,d=dx*dx+dy*dy,r=RS(n)+3;if(d<r*r&&d<bd){bd=d;best=n;}}
|
|
425
|
+
return best;
|
|
426
|
+
}
|
|
427
|
+
let down=null,dragN=null,moved=false;
|
|
428
|
+
cv.addEventListener("pointerdown",ev=>{const r=cv.getBoundingClientRect(),px=ev.clientX-r.left,py=ev.clientY-r.top,n=pick(px,py);
|
|
429
|
+
down={px,py,tx:T.x,ty:T.y,n};moved=false;cv.setPointerCapture(ev.pointerId);anim=null;
|
|
430
|
+
if(n){dragN=n;}else cv.classList.add("drag");});
|
|
431
|
+
cv.addEventListener("pointermove",ev=>{const r=cv.getBoundingClientRect(),px=ev.clientX-r.left,py=ev.clientY-r.top;
|
|
432
|
+
if(down){const dx=px-down.px,dy=py-down.py;if(Math.abs(dx)+Math.abs(dy)>3)moved=true;
|
|
433
|
+
if(dragN&&moved){const w=toWorld(px,py);dragN.x=dragN.tx=w.x;dragN.y=dragN.ty=w.y;if((MAPS.mode==="force"||MAPS.mode==="net")&&MAPS.sim){dragN.fx=w.x;dragN.fy=w.y;MAPS.sim.alpha=Math.max(MAPS.sim.alpha,.25);}dirty=true;}
|
|
434
|
+
else if(!dragN&&moved){T.x=down.tx+dx;T.y=down.ty+dy;dirty=true;}
|
|
435
|
+
return;}
|
|
436
|
+
const n=pick(px,py);if((n&&n.id)!==hover){hover=n?n.id:null;dirty=true;cv.classList.toggle("hit",!!n);}
|
|
437
|
+
if(n){const ep=EPBY[n.id];tip.style.display="block";tip.style.left=Math.min(W-300,px+14)+"px";tip.style.top=Math.min(H-70,py+14)+"px";
|
|
438
|
+
tip.innerHTML=`<b>${esc(n.label)}</b><span>${esc(KIND_LABEL[n.type]||humanize(n.type))}${ep?" · "+esc(ep.state||""):""}${n.deg?" · "+n.deg+" link"+(n.deg===1?"":"s"):""}</span>`;}
|
|
439
|
+
else tip.style.display="none";});
|
|
440
|
+
cv.addEventListener("pointerup",ev=>{
|
|
441
|
+
if(dragN){dragN.fx=dragN.fy=null;if(!moved){if(ev.shiftKey&&MAPS.sel&&MAPS.sel!==dragN.id){setPath(MAPS.sel,dragN.id);}else{MAPS.path=null;MAPS.sel=dragN.id;if(MAPS.mode==="focus"){MAPS.focusId=dragN.id;layout();fit(true);}}recompute();inspector();dirty=true;}}
|
|
442
|
+
else if(down&&!moved){MAPS.sel=null;MAPS.path=null;recompute();inspector();dirty=true;}
|
|
443
|
+
down=null;dragN=null;cv.classList.remove("drag");});
|
|
444
|
+
cv.addEventListener("pointerleave",()=>{hover=null;tip.style.display="none";dirty=true;});
|
|
445
|
+
cv.addEventListener("wheel",ev=>{ev.preventDefault();const r=cv.getBoundingClientRect();zoomAt(ev.deltaY<0?1.15:1/1.15,ev.clientX-r.left,ev.clientY-r.top);},{passive:false});
|
|
446
|
+
cv.addEventListener("dblclick",ev=>{const r=cv.getBoundingClientRect(),n=pick(ev.clientX-r.left,ev.clientY-r.top);if(n){MAPS.mode="focus";MAPS.focusId=n.id;MAPS.sel=n.id;mapSave();render();}else fit(true);});
|
|
447
|
+
mini.addEventListener("pointerdown",ev=>{const r=mini.getBoundingClientRect(),m=mini._m;if(!m)return;const mx=(ev.clientX-r.left)*(mini.width/r.width),my=(ev.clientY-r.top)*(mini.height/r.height);
|
|
448
|
+
const wx=(mx-m.ox)/m.k,wy=(my-m.oy)/m.k;anim={from:{...T},to:{k:T.k,x:W/2-wx*T.k,y:H/2-wy*T.k},t0:performance.now(),dur:300};dirty=true;});
|
|
449
|
+
$$("[data-z]",wrap).forEach(b=>b.onclick=()=>{const k=b.dataset.z;if(k==="fit")fit(true);else zoomAt(k==="in"?1.3:1/1.3,W/2,H/2);});
|
|
450
|
+
$("#mapx").addEventListener("keydown",ev=>{if(ev.target.tagName==="INPUT")return;
|
|
451
|
+
if(ev.key==="f"||ev.key==="F")fit(true);else if(ev.key==="Escape"){MAPS.sel=null;MAPS.path=null;recompute();inspector();dirty=true;}
|
|
452
|
+
else if(ev.key==="+"||ev.key==="=")zoomAt(1.2,W/2,H/2);else if(ev.key==="-")zoomAt(1/1.2,W/2,H/2);});
|
|
453
|
+
|
|
454
|
+
/* ---- toolbar ---- */
|
|
455
|
+
$$("[data-layer]").forEach(b=>b.onclick=()=>{const k=b.dataset.layer;MAPS.hidden.has(k)?MAPS.hidden.delete(k):MAPS.hidden.add(k);mapSave();MAPS.sel=MAPS.path=null;render();});
|
|
456
|
+
$$("[data-es]").forEach(b=>b.onclick=()=>{const k=b.dataset.es;MAPS.edgeOff.has(k)?MAPS.edgeOff.delete(k):MAPS.edgeOff.add(k);mapSave();render();});
|
|
457
|
+
$$("[data-mode]").forEach(b=>b.onclick=()=>{if(MAPS.mode!==b.dataset.mode){MAPS.mode=b.dataset.mode;if(MAPS.mode==="focus"&&MAPS.sel)MAPS.focusId=MAPS.sel;mapSave();render();}});
|
|
458
|
+
$("#mpng").onclick=()=>{const c=document.createElement("canvas");c.width=cv.width*2;c.height=cv.height*2;const x=c.getContext("2d");x.fillStyle=C.ink;x.fillRect(0,0,c.width,c.height);x.drawImage(cv,0,0,c.width,c.height);c.toBlob(b=>{const u=URL.createObjectURL(b),a=document.createElement("a");a.href=u;a.download=(D.app||"model")+".map.png";a.click();URL.revokeObjectURL(u);});};
|
|
459
|
+
/* search with suggestions */
|
|
460
|
+
const sq=$("#msq"),sg=$("#msug");let sgi=0,sgl=[];
|
|
461
|
+
function suggest(){const q=sq.value.trim().toLowerCase();recompute();dirty=true;if(!q){sg.hidden=true;return;}
|
|
462
|
+
sgl=nodes.filter(n=>String(n.label).toLowerCase().includes(q)).sort((a,b)=>String(a.label).toLowerCase().indexOf(q)-String(b.label).toLowerCase().indexOf(q)||b.deg-a.deg).slice(0,8);sgi=0;
|
|
463
|
+
sg.hidden=!sgl.length;sg.innerHTML=sgl.map((n,i)=>`<button data-i="${i}" class="${i?"":"on"}"><span class="sw" style="width:8px;height:8px;border-radius:2px;background:${CAT[n.cat]._c}"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(n.label)}</span><span class="k">${esc(n.type)}</span></button>`).join("");
|
|
464
|
+
$$("button",sg).forEach(b=>b.onmousedown=e=>{e.preventDefault();choose(sgl[+b.dataset.i]);});}
|
|
465
|
+
function choose(n){if(!n)return;sg.hidden=true;MAPS.sel=n.id;MAPS.path=null;if(MAPS.mode==="focus"){MAPS.focusId=n.id;layout();fit(true);}else flyTo(n);recompute();inspector();dirty=true;}
|
|
466
|
+
sq.addEventListener("input",suggest);sq.addEventListener("blur",()=>setTimeout(()=>sg.hidden=true,120));
|
|
467
|
+
sq.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();choose(sgl[sgi]);}else if(e.key==="ArrowDown"||e.key==="ArrowUp"){e.preventDefault();sgi=(sgi+(e.key==="ArrowDown"?1:-1)+sgl.length)%Math.max(1,sgl.length);$$("button",sg).forEach((b,i)=>b.classList.toggle("on",i===sgi));}else if(e.key==="Escape"){sq.value="";suggest();}});
|
|
468
|
+
|
|
469
|
+
/* ---- inspector ---- */
|
|
470
|
+
const invKey=n=>{const ep=EPBY[n.id];if(ep&&ep.state!=="STATIC_ONLY")return(ep.host||"")+(ep.path||"").replace(/\/$/,"");if(n.type==="route")return n.id.replace(/^route:/,"").replace(/\/$/,"");if(n.type==="host")return n.label;return null;};
|
|
471
|
+
function inspector(){
|
|
472
|
+
const el=$("#minsp"),n=MAPS.sel?byId.get(MAPS.sel):null;
|
|
473
|
+
if(!n||!nset.has(n.id)){
|
|
474
|
+
const hubs=nodes.slice().sort((a,b)=>b.deg-a.deg).slice(0,8);
|
|
475
|
+
const cnt=CAT.map((c,i)=>[c,nodes.filter(x=>x.cat===i).length]).filter(x=>x[1]);
|
|
476
|
+
el.innerHTML=`<h3>This map</h3><div class="nrows"><div class="k">Nodes</div><div class="v">${nodes.length}</div><div class="k">Edges</div><div class="v">${edges.length}</div>
|
|
477
|
+
<div class="k">Seen in traffic</div><div class="v">${edges.filter(e=>e.state==="OBSERVED").length}</div><div class="k">Found in code</div><div class="v">${edges.filter(e=>e.state==="INFERRED").length}</div>
|
|
478
|
+
${edges.some(e=>e.state==="EXTERNAL")?`<div class="k">From recon</div><div class="v">${edges.filter(e=>e.state==="EXTERNAL").length}</div>`:""}</div>
|
|
479
|
+
<h3>Most connected</h3>${hubs.map(h=>`<div class="hubrow" data-pick="${esc(h.id)}"><span class="s" style="width:9px;height:9px;border-radius:2px;background:${CAT[h.cat]._c};flex:none"></span><span class="t">${esc(h.label)}</span><span class="d">${h.deg}</span></div>`).join("")}
|
|
480
|
+
<h3>How to read it</h3><div class="tips">Hover a node to trace what it touches. <b>Click</b> to inspect it. <kbd>Shift</kbd>+click a second node to trace the shortest path between them. <b>Double-click</b> a node to focus on it. Drag a node to move it. Scroll to zoom, <kbd>F</kbd> to fit.<br>Solid lines were seen in traffic, dashed lines were found in the code, dotted lines come from recon. A dashed hollow node is an endpoint the code names but no request reached.</div>`;
|
|
481
|
+
$$("[data-pick]",el).forEach(r=>r.onclick=()=>choose(byId.get(r.dataset.pick)));return;
|
|
482
|
+
}
|
|
483
|
+
const ep=EPBY[n.id],cat=CAT[n.cat];
|
|
484
|
+
const groups={};(adj.get(n.id)||[]).forEach(x=>{const k=(x.out?"→ ":"← ")+x.e.type+"|"+x.e.state;(groups[k]=groups[k]||[]).push(x.n);});
|
|
485
|
+
const rows=[];
|
|
486
|
+
if(ep){rows.push(["State",ep.state==="BOTH"?"seen in code and traffic":ep.state==="STATIC_ONLY"?"in code, never called":"traffic only"]);if(ep.statuses.length)rows.push(["Statuses",ep.statuses.join(", ")]);rows.push(["Requests",ep.requests]);if(ep.params.length)rows.push(["Parameters",ep.params.length]);if(ep.roles.length)rows.push(["Roles",ep.roles.join(", ")]);if(ep.credentials.length)rows.push(["Credentials",ep.credentials.join(", ")]);if(ep.privileged)rows.push(["Note","privileged-looking path"]);}
|
|
487
|
+
else if(n.state)rows.push(["State",n.state]);
|
|
488
|
+
if((n.evidence||[]).length)rows.push(["Evidence",n.evidence.slice(0,4).map(EVD).join(", ")+(n.evidence.length>4?" …":"")]);
|
|
489
|
+
if(n.detail)rows.push(["Detail",n.detail]);
|
|
490
|
+
const path=MAPS.path;
|
|
491
|
+
el.innerHTML=`<div class="nh"><span class="ic" style="color:${cat._c}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ICON[n.type]||ICON.endpoint}"/></svg></span><span class="kind">${esc(KIND_LABEL[n.type]||humanize(n.type))}</span></div>
|
|
492
|
+
<div class="nlabel">${esc(n.label)}</div>
|
|
493
|
+
<div class="nrows">${rows.map(([k,v])=>`<div class="k">${esc(k)}</div><div class="v">${esc(v)}</div>`).join("")}<div class="k">Links</div><div class="v">${n.deg}</div></div>
|
|
494
|
+
<div class="nact"><button class="mbtn" id="nfocus">Focus</button>${ep||(n.evidence||[]).length?`<button class="mbtn" id="ndet">Details</button>`:""}${BQLDB?`<button class="mbtn" id="nreach">Query reach</button>`:""}${invKey(n)!=null?`<button class="mbtn" id="ninv">Inventory</button>`:""}</div>
|
|
495
|
+
${path?`<div class="pathbox"><div class="pt">Shortest path</div>${path.ids?path.ids.map((id,i)=>`<div class="conn" data-pick="${esc(id)}"><span class="a">${i?"↳":"●"}</span><span class="t">${esc((byId.get(id)||{}).label||id)}</span></div>`).join(""):`<span style="font-size:12.5px;color:var(--muted)">No path between these two nodes with the current filters.</span>`}</div>`:""}
|
|
496
|
+
${Object.keys(groups).sort().map(k=>{const [lab,st]=k.split("|");const cls=st==="INFERRED"?"in":st==="EXTERNAL"?"ex":"ob";return `<div class="ghd"><span>${esc(lab.replace(/_/g," ").toLowerCase())}</span><span class="st ${cls}">${st==="INFERRED"?"code":st==="EXTERNAL"?"recon":"traffic"}</span><span>${groups[k].length}</span></div>${groups[k].slice(0,30).map(m=>`<div class="conn" data-pick="${esc(m.id)}"><span class="s" style="background:${CAT[m.cat]._c}"></span><span class="t">${esc(m.label)}</span></div>`).join("")}${groups[k].length>30?`<div class="conn"><span class="t" style="color:var(--faint)">+${groups[k].length-30} more</span></div>`:""}`;}).join("")}`;
|
|
497
|
+
$$("[data-pick]",el).forEach(r=>r.onclick=()=>choose(byId.get(r.dataset.pick)));
|
|
498
|
+
const f=$("#nfocus");if(f)f.onclick=()=>{MAPS.mode="focus";MAPS.focusId=n.id;mapSave();render();};
|
|
499
|
+
const d=$("#ndet");if(d)d.onclick=()=>openNode(n.id);
|
|
500
|
+
const r=$("#nreach");if(r)r.onclick=()=>{if(BQLDB){qRun(`reach "${n.label.replace(/"/g,'\\"')}"`);aNL=null;view="ask";render();}};
|
|
501
|
+
const iv=$("#ninv");if(iv)iv.onclick=()=>{INV.node=invKey(n);INV.q="";INV.sel=null;view="inventory";render();};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* ---- go ---- */
|
|
505
|
+
resize();layout();
|
|
506
|
+
nodes.forEach(n=>{n.x=n.tx;n.y=n.ty;});
|
|
507
|
+
if(MAPS.mode==="focus"&&!MAPS.sel&&MAPS.focusId==null){}
|
|
508
|
+
fit(false);recompute();inspector();
|
|
509
|
+
if(MAPS.sel&&nset.has(MAPS.sel)&&MAPS.mode!=="focus"){}
|
|
510
|
+
new ResizeObserver(()=>{resize();}).observe(wrap);
|
|
511
|
+
MAPS.dbg=()=>({T,nodes,W,H});
|
|
512
|
+
MAPS.raf=requestAnimationFrame(frame);
|
|
513
|
+
if(!nodes.length){$("#mhud").textContent="Every layer is hidden. Turn one back on above.";}
|
|
514
|
+
}
|
|
515
|
+
"""
|