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.
Files changed (66) hide show
  1. proxy2model/__init__.py +19 -0
  2. proxy2model/__main__.py +6 -0
  3. proxy2model/atomic.py +68 -0
  4. proxy2model/cli/__init__.py +3 -0
  5. proxy2model/cli/analyze.py +501 -0
  6. proxy2model/cli/app.py +408 -0
  7. proxy2model/cli/browse.py +279 -0
  8. proxy2model/cli/build.py +399 -0
  9. proxy2model/cli/common.py +190 -0
  10. proxy2model/cli/history.py +224 -0
  11. proxy2model/cli/mcp.py +184 -0
  12. proxy2model/cli/ui.py +62 -0
  13. proxy2model/core/__init__.py +1 -0
  14. proxy2model/core/changes.py +124 -0
  15. proxy2model/core/context.py +571 -0
  16. proxy2model/core/diff.py +330 -0
  17. proxy2model/core/domains.py +98 -0
  18. proxy2model/core/graph.py +562 -0
  19. proxy2model/core/methodology.py +343 -0
  20. proxy2model/core/model.py +998 -0
  21. proxy2model/core/stack.py +218 -0
  22. proxy2model/data/fonts/LICENSES.md +16 -0
  23. proxy2model/data/fonts/inter-latin-wght-normal.woff2 +0 -0
  24. proxy2model/data/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  25. proxy2model/data/fonts/nippo-200.woff2 +0 -0
  26. proxy2model/data/fonts/nippo-300.woff2 +0 -0
  27. proxy2model/data/fonts/nippo-400.woff2 +0 -0
  28. proxy2model/data/fonts/nippo-500.woff2 +0 -0
  29. proxy2model/data/fonts/nippo-700.woff2 +0 -0
  30. proxy2model/data/public_suffix_list.dat +16497 -0
  31. proxy2model/db/__init__.py +1 -0
  32. proxy2model/db/bql.py +710 -0
  33. proxy2model/db/bql_js.py +278 -0
  34. proxy2model/db/store.py +547 -0
  35. proxy2model/export/__init__.py +1 -0
  36. proxy2model/export/openapi.py +248 -0
  37. proxy2model/ingest/__init__.py +1 -0
  38. proxy2model/ingest/browse.py +181 -0
  39. proxy2model/ingest/browser.py +1058 -0
  40. proxy2model/ingest/crawl.py +673 -0
  41. proxy2model/ingest/extract.py +208 -0
  42. proxy2model/ingest/infer.py +75 -0
  43. proxy2model/ingest/journey.py +148 -0
  44. proxy2model/ingest/live.py +96 -0
  45. proxy2model/ingest/parse.py +1275 -0
  46. proxy2model/ingest/redact.py +535 -0
  47. proxy2model/py.typed +0 -0
  48. proxy2model/recon/__init__.py +1 -0
  49. proxy2model/recon/osint.py +607 -0
  50. proxy2model/report/__init__.py +1 -0
  51. proxy2model/report/animate.py +163 -0
  52. proxy2model/report/dash.py +469 -0
  53. proxy2model/report/fonts.py +51 -0
  54. proxy2model/report/html.py +988 -0
  55. proxy2model/report/inventory.py +258 -0
  56. proxy2model/report/map.py +515 -0
  57. proxy2model/report/pages.py +537 -0
  58. proxy2model/report/query.py +306 -0
  59. proxy2model/report/shot.py +55 -0
  60. proxy2model/report/view.py +213 -0
  61. proxy2model-2.0.0.dist-info/METADATA +148 -0
  62. proxy2model-2.0.0.dist-info/RECORD +66 -0
  63. proxy2model-2.0.0.dist-info/WHEEL +5 -0
  64. proxy2model-2.0.0.dist-info/entry_points.txt +2 -0
  65. proxy2model-2.0.0.dist-info/licenses/LICENSE +201 -0
  66. proxy2model-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,537 @@
1
+ """Pages with a request/response viewer underneath: To check, and Reference (the layered sunburst).
2
+
3
+ Every list on these pages is master-detail: pick a row and the masked request and response for it
4
+ appear in the viewer at the bottom, the way Burp's HTTP history works. The Reference page peels the
5
+ app in layers: click a ring of the chart and the table below lists what is in it.
6
+ """
7
+
8
+ PG_CSS = r"""
9
+ /* split pages: content on top, viewer below */
10
+ .m-priorities,.m-endpoints,.m-reference,.m-ask{padding:0!important;overflow:hidden!important}
11
+ .m-priorities .view,.m-endpoints .view,.m-reference .view,.m-ask .view{max-width:none;height:100%}
12
+ .m-priorities .foot,.m-endpoints .foot,.m-reference .foot,.m-ask .foot{display:none}
13
+ .splitpg{display:flex;flex-direction:column;height:100%;min-height:0}
14
+ .splitpg .top{flex:1 1 auto;min-height:140px;overflow:auto;padding:24px 32px 28px}
15
+ .splitpg .gut{height:4px;flex:none;background:var(--line);cursor:row-resize;position:relative;z-index:2}
16
+ .splitpg .gut:hover,.splitpg .gut.drag{background:var(--signal)}
17
+ .splitpg .bot{flex:none;min-height:190px;max-height:80%;border-top:1px solid var(--line);background:var(--panel)}
18
+ .splitpg .gut[hidden],.splitpg .bot[hidden]{display:none}
19
+ .rrnote{font:500 11.5px var(--sans);color:var(--faint);font-style:italic}
20
+ .rrp .code .l.hit{background:var(--signal-soft);box-shadow:inset 3px 0 var(--signal)}
21
+ .ptab{font:600 11px var(--mono);padding:3px 9px;border-radius:5px;border:1px solid transparent;background:none;color:var(--muted)}
22
+ .ptab.on{background:var(--panel);border-color:var(--line2);color:var(--text)}
23
+ .arow.sel{background:var(--signal-soft);border-color:var(--signal)}
24
+ .qtab tr.sel{background:var(--signal-soft);box-shadow:inset 3px 0 var(--signal)}
25
+ /* tabs and chips */
26
+ .tabs{display:flex;gap:4px;margin:6px 0 16px;border-bottom:1px solid var(--line)}
27
+ .tabs button{font:600 13px var(--sans);padding:9px 16px;border:0;background:none;color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px}
28
+ .tabs button.on{color:var(--text);border-bottom-color:var(--signal)}
29
+ .tabs button .n{font:600 11px var(--mono);color:var(--faint);margin-left:6px}
30
+ .fchips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;align-items:center}
31
+ .fchip{font:600 12px var(--sans);padding:5px 12px;border-radius:999px;border:1px solid var(--line2);background:var(--panel);color:var(--muted)}
32
+ .fchip.on{background:var(--text);color:var(--ink);border-color:var(--text)}
33
+ .fchip .n{font:600 11px var(--mono);margin-left:5px;opacity:.7}
34
+ /* leads */
35
+ .lrow{display:grid;grid-template-columns:34px minmax(0,1fr);gap:0 12px;background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--c);border-radius:12px;padding:13px 16px 12px 12px;margin-bottom:9px;cursor:pointer}
36
+ .lrow:hover{border-color:var(--line2);border-left-color:var(--c)}
37
+ .lrow.sel{background:var(--panel2);border-color:var(--c);box-shadow:var(--shadow)}
38
+ .lrow .rk{font:700 13px var(--mono);color:var(--c);text-align:right;padding-top:2px}
39
+ .lrow .t{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
40
+ .lrow .p{font:700 13.5px var(--mono);word-break:break-all}
41
+ .lrow .sevb{font:700 9.5px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--c);border:1px solid var(--c);border-radius:5px;padding:1px 6px}
42
+ .lrow .w{font:600 13.5px var(--sans);margin-top:5px}
43
+ .lrow .y{color:var(--muted);font-size:12.5px;margin-top:3px;max-width:90ch}
44
+ .lrow .nx{font-size:12.5px;margin-top:6px}
45
+ .lrow .nx b{font:700 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--c);margin-right:6px}
46
+ .lrow .more{color:var(--faint);font-size:12px;margin-top:4px}
47
+ .sev-hot{--c:var(--signal)}.sev-warn{--c:var(--warn)}.sev-info{--c:var(--observed)}
48
+ /* leads that share one reason: the reason once, then the endpoints */
49
+ .lgrp{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--c);border-radius:12px;padding:13px 16px 8px;margin-bottom:12px}
50
+ .lgrp .lgh{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
51
+ .lgrp .lgh b{font:600 14px var(--sans)}
52
+ .lgrp .lgh .n{font:600 11px var(--mono);color:var(--faint)}
53
+ .lgrp .y{color:var(--muted);font-size:12.5px;margin-top:4px;max-width:90ch}
54
+ .lgrp .nx{font-size:12.5px;margin:6px 0 8px}
55
+ .lgrp .nx b{font:700 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--c);margin-right:6px}
56
+ .lgrp .sevb,.qgh .sevb{font:700 9.5px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--c);border:1px solid var(--c);border-radius:5px;padding:1px 6px}
57
+ .lrow.c{grid-template-columns:28px minmax(0,1fr);border:0;border-top:1px solid var(--line);border-radius:0;margin:0 -16px 0 -16px;padding:8px 16px 8px 12px;background:none}
58
+ .lrow.c:hover{background:var(--panel2)}
59
+ .lrow.c.sel{background:var(--signal-soft);box-shadow:inset 3px 0 var(--c)}
60
+ .lrow.c .rk{font-size:12px;padding-top:0}
61
+ .lrow.c .more{margin:0 0 0 6px;display:inline}
62
+ /* endpoints table */
63
+ .epscroll{overflow-x:auto;border-radius:12px}
64
+ .eptab th.srt{cursor:pointer;user-select:none}
65
+ .eptab th.srt:hover,.eptab th.srt.on{color:var(--text)}
66
+ .eptab .sm{font-size:11px;margin-top:2px}
67
+ .eptab .sevb{font:700 9.5px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--c);border:1px solid var(--c);border-radius:5px;padding:1px 6px;white-space:nowrap}
68
+ .seen{font:600 11px var(--mono);padding:2px 7px;border-radius:6px;white-space:nowrap}
69
+ .seen.s-traffic{color:var(--observed);background:var(--panel2)}
70
+ .seen.s-both{color:var(--ok);background:var(--panel2)}
71
+ .seen.s-code{color:var(--signal);border:1px dashed var(--signal)}
72
+ .dhead .fchips+input{margin-left:auto}
73
+ /* open questions */
74
+ .qgh{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:12px 16px;margin:4px 0 10px}
75
+ .qgh .un{color:var(--l6);font-size:12.5px}
76
+ .qgh .nx{margin-top:6px;font-size:12.5px}
77
+ .qgh .nx b{font:700 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--l6);margin-right:6px}
78
+ .qgh .tip{margin-top:8px;font-size:12.5px;color:var(--text)}
79
+ .qrow.c{padding:8px 14px;margin-bottom:6px}
80
+ .qrow.c ul{margin-top:3px}
81
+ .qgrp{margin:18px 0 8px;font:700 13px var(--display,var(--sans));display:flex;gap:8px;align-items:baseline}
82
+ .qgrp .n{font:600 11px var(--mono);color:var(--faint)}
83
+ .qrow{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px 16px;margin-bottom:8px;cursor:pointer}
84
+ .qrow:hover{border-color:var(--line2)}
85
+ .qrow.sel{background:var(--panel2);border-color:var(--l6)}
86
+ .qrow .e{font:700 13px var(--mono);word-break:break-all}
87
+ .qrow ul{margin:6px 0 0;padding-left:18px;color:var(--muted);font-size:12.5px}
88
+ .qrow .un{color:var(--l6)}
89
+ .qrow .nx{margin-top:7px;font-size:12.5px}
90
+ .qrow .nx b{font:700 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--l6);margin-right:6px}
91
+ /* reference: the layered chart */
92
+ .splitpg .top.refpg{display:flex;flex-direction:column;overflow:hidden;padding-bottom:0}
93
+ .refhd{flex:none}
94
+ .refgrid{flex:1;min-height:0;display:grid;grid-template-columns:minmax(300px,460px) minmax(0,1fr);gap:30px}
95
+ .refl,.refr{min-height:0;overflow:auto;padding-bottom:24px}
96
+ @media(max-width:1000px){.splitpg .top.refpg{overflow:auto}.refgrid{grid-template-columns:minmax(0,1fr)}.refl,.refr{overflow:visible}}
97
+ .sunwrap{position:relative;width:min(460px,100%,calc(58vh - 190px));min-width:260px;margin:0 auto 12px}
98
+ .sunwrap svg{width:100%;height:auto;display:block}
99
+ .sun path{stroke:var(--panel);stroke-width:1.2;cursor:pointer;transition:opacity .12s}
100
+ .sun path:hover{opacity:.82}
101
+ .sun path.pick{stroke:var(--text);stroke-width:2.2}
102
+ .sun text{font:600 10.5px var(--sans);fill:#fff;pointer-events:none;paint-order:stroke;stroke:rgba(0,0,0,.18);stroke-width:2px}
103
+ .sun .ctr{fill:var(--panel);stroke:var(--line2);stroke-width:1.5;cursor:pointer}
104
+ .sun .ctr:hover{fill:var(--panel2)}
105
+ .sun .ct1{font:700 15px var(--display,var(--sans));fill:var(--text);stroke:none;text-anchor:middle}
106
+ .sun .ct2{font:500 10.5px var(--sans);fill:var(--muted);stroke:none;text-anchor:middle}
107
+ .suntip{position:absolute;pointer-events:none;background:var(--panel);border:1px solid var(--line2);border-radius:9px;box-shadow:var(--shadow);padding:6px 10px;font:12px var(--mono);display:none;z-index:4;max-width:280px}
108
+ .suntip b{display:block;font:700 12.5px var(--mono);word-break:break-all}
109
+ .suntip span{color:var(--muted);font-size:11px}
110
+ .crumbs2{display:flex;gap:4px;flex-wrap:wrap;align-items:center;font:600 12px var(--mono);color:var(--muted);margin-bottom:10px}
111
+ .crumbs2 button{border:0;background:none;color:var(--muted);font:inherit;padding:2px 4px;border-radius:5px}
112
+ .crumbs2 button:hover{color:var(--signal)}
113
+ .crumbs2 .cur{color:var(--text)}
114
+ .reft h2{font:700 24px var(--display,var(--sans));letter-spacing:-.03em;margin:0 0 6px}
115
+ .reft p{color:var(--muted);font-size:14px;line-height:1.6;max-width:62ch;margin:0 0 14px}
116
+ .refkids{display:flex;gap:8px;flex-wrap:wrap}
117
+ .refkid{display:inline-flex;align-items:center;gap:7px;font:600 12.5px var(--sans);padding:6px 12px;border-radius:9px;border:1px solid var(--line2);background:var(--panel);color:var(--text)}
118
+ .refkid:hover{border-color:var(--signal)}
119
+ .refkid i{width:9px;height:9px;border-radius:3px;background:var(--c)}
120
+ .refkid .n{font:600 11px var(--mono);color:var(--faint)}
121
+ .layers{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px}
122
+ .layer{background:var(--panel);border:1px solid var(--line);border-top:3px solid var(--c);border-radius:12px;padding:14px 16px;text-align:left;cursor:pointer}
123
+ .layer:hover{border-color:var(--c);box-shadow:var(--shadow)}
124
+ .layer{display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start}
125
+ .layer .nn{font:700 26px var(--display,var(--sans));letter-spacing:-.03em}
126
+ .layer b{display:block;font:700 13.5px var(--sans);margin:2px 0}
127
+ .layer span{color:var(--muted);font-size:12.5px}
128
+ .dtable{width:100%;border-collapse:collapse;font-size:13px;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
129
+ .dtable th{position:static;text-align:left;font:600 10.5px var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted);background:var(--panel2);padding:8px 12px;border-bottom:1px solid var(--line);cursor:default}
130
+ .dtable td{padding:8px 12px;border-bottom:1px solid var(--line);vertical-align:middle}
131
+ .dtable tbody tr{cursor:pointer}
132
+ .dtable tbody tr:last-child td{border-bottom:0}
133
+ .dtable tbody tr:hover{background:var(--panel2)}
134
+ .dtable tbody tr.sel{background:var(--signal-soft);box-shadow:inset 3px 0 var(--signal)}
135
+ .dtable .mono{font:12.5px var(--mono)}
136
+ .dtable .dim{color:var(--muted)}
137
+ .dhead{display:flex;align-items:center;gap:12px;margin:0 0 10px;flex-wrap:wrap}
138
+ .dhead h3{font:700 16px var(--display,var(--sans));margin:0;letter-spacing:-.01em}
139
+ .dhead .n{color:var(--faint);font:600 12px var(--mono)}
140
+ .dhead input{margin-left:auto;height:32px;min-width:220px;padding:0 10px;border:1px solid var(--line2);border-radius:8px;background:var(--panel);color:var(--text);font:12.5px var(--mono)}
141
+ .dhead input:focus{border-color:var(--signal);outline:none}
142
+ .pickinfo{margin:12px 0 4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center}
143
+ .pickinfo .k{font:600 10px var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--faint);margin-right:2px}
144
+ .pickinfo .c{font:12px var(--mono);padding:3px 9px;border-radius:7px;background:var(--panel2);border:1px solid var(--line);color:var(--text)}
145
+ .pickinfo button.c:hover{border-color:var(--signal);color:var(--signal)}
146
+ .dsec{margin-bottom:22px}
147
+ .stackrow{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px;margin-bottom:18px}
148
+ """
149
+
150
+ PG_JS = r"""
151
+ /* ---------- split page helpers ---------- */
152
+ const SPL=(function(){let h=.4;try{const v=parseFloat(localStorage.getItem("b2m-split"));if(v>.15&&v<.8)h=v;}catch(e){}return{h};})();
153
+ function splitShell(inner,cls){return `<div class="splitpg" id="splitpg"><div class="top ${cls||""}" id="sptop">${inner}</div><div class="gut" id="spgut" hidden></div><div class="bot" id="spbot" hidden style="height:${Math.round(SPL.h*100)}%"></div></div>`;}
154
+ function highlightIn(el,text){
155
+ if(!text)return;
156
+ const lines=[...el.querySelectorAll(".rrp:last-child .code .l")];
157
+ const hit=lines.find(l=>l.textContent.includes(text));
158
+ if(!hit){const t=$(".rrt",el);if(t&&!$(".rrnote",el))t.insertAdjacentHTML("afterend",`<span class="rrnote">named past the first 40 KB of this script, which is all the report keeps; search the script itself for ${esc(text)}</span>`);return;}
159
+ hit.classList.add("hit");
160
+ const box=hit.closest(".code");if(box)box.scrollTop=Math.max(0,hit.offsetTop-box.clientHeight/3);
161
+ }
162
+ function splitShow(ids,opts){
163
+ const bot=$("#spbot"),gut=$("#spgut");if(!bot)return;
164
+ opts=opts||{};
165
+ if((!ids||!ids.length)&&!opts.empty){bot.hidden=gut.hidden=true;return;}
166
+ bot.hidden=gut.hidden=false;
167
+ rrMount(bot,ids||[],opts);
168
+ if(opts.find)highlightIn(bot,opts.find);
169
+ }
170
+ function splitInit(){
171
+ const g=$("#spgut"),root=$("#splitpg");if(!g)return;
172
+ g.onpointerdown=ev=>{ev.preventDefault();g.classList.add("drag");g.setPointerCapture(ev.pointerId);
173
+ g.onpointermove=e=>{const b=root.getBoundingClientRect();SPL.h=Math.max(.16,Math.min(.78,(b.bottom-e.clientY)/b.height));$("#spbot").style.height=Math.round(SPL.h*100)+"%";};
174
+ g.onpointerup=()=>{g.classList.remove("drag");g.onpointermove=g.onpointerup=null;try{localStorage.setItem("b2m-split",String(SPL.h));}catch(e){}};};
175
+ }
176
+ /* old section names keep working as links */
177
+ const LEGACY={surface:["endpoints"],code:["reference","scripts"],supply:["reference","parties"],trust:["reference","trust"],
178
+ crossrole:["reference","roles"],infra:["reference","infra"],unknowns:["priorities","open"],changes:["priorities","changes"],query:["ask"],evidence:["inventory"]};
179
+ function go(id){
180
+ const l=LEGACY[id];
181
+ if(!l){view=id;return;}
182
+ view=l[0];
183
+ if(l[0]==="reference"){REF.focusId=l[1];REF.pickId=null;}
184
+ if(l[0]==="priorities")CHK.tab=l[1];
185
+ }
186
+ function evForLabel(label){
187
+ if(!label)return [];
188
+ const ep=EP.find(e=>e.label===label||e.path===label||(e.method+" "+e.path)===label);if(ep)return ep.evidence||[];
189
+ const s=(D.scripts||[]).find(x=>x.label===label);if(s)return s.evidence||[];
190
+ const r=(D.routes||[]).find(x=>x.label===label);if(r)return r.evidence||[];
191
+ const t=(D.third_parties||[]).find(x=>x.label===label);if(t)return t.evidence||[];
192
+ const c=(D.cookies||[]).find(x=>x.name===label);if(c)return c.evidence||[];
193
+ return [];
194
+ }
195
+
196
+ /* ---------- TO CHECK: leads and open questions in one place ---------- */
197
+ const CHK={tab:"leads",sev:"all",type:"all",sel:null};
198
+ function chkLeads(){
199
+ const items=PRIO.map(p=>({sev:p.sev,ep:p.ep,lead:p.sig[0],more:p.sig.slice(1),ids:p.ep.evidence||[],find:p.ep.state==="STATIC_ONLY"?p.ep.path:null,title:epTitle(p.ep)}));
200
+ weakCookies.forEach(k=>{const miss=[!k.httponly&&"HttpOnly",!k.secure&&"Secure"].filter(Boolean);
201
+ items.push({sev:"warn",cookie:k,lead:{ttl:`Cookie set without ${miss.join(" and ")}`,why:`SameSite is ${k.samesite||"unset"}. A cookie without ${miss.join(" or ")} is easier to steal or use cross-site.`,next:"Confirm whether this cookie carries a session or auth state."},more:[],ids:k.evidence||[],title:"cookie "+k.name});});
202
+ return items;
203
+ }
204
+ function chkLeadRow(it,i){
205
+ const e=it.ep,col={hot:"High",warn:"Medium",info:"Note"}[it.sev];
206
+ return `<div class="lrow sev-${it.sev}${CHK.sel===i?" sel":""}" data-lead="${i}"><div class="rk">${i+1}</div><div>
207
+ <div class="t"><span class="sevb">${col}</span>${e?methodm(e.method)+`<span class="p">${esc(e.path)}</span>`:`<span class="p">cookie ${esc(it.cookie.name)}</span>`}</div>
208
+ <div class="w">${esc(it.lead.ttl)}</div><div class="y">${esc(it.lead.why)}</div><div class="nx"><b>Do next</b>${esc(it.lead.next)}</div>
209
+ ${it.more.length?`<div class="more">+ ${it.more.map(s=>esc(s.ttl)).join(" · ")}</div>`:""}</div></div>`;
210
+ }
211
+ function vPriorities(){
212
+ const leads=chkLeads(),open=D.unknowns||[];
213
+ const body=CHK.tab==="changes"&&hasChanges()?chkChangesHtml():CHK.tab==="open"?chkOpenHtml(open):chkLeadsHtml(leads);
214
+ return splitShell(`<h1 class="vt">To check</h1><p class="vsub">Leads worth a look first, and the questions this capture could not answer. Pick one to see the requests behind it below.</p>
215
+ <div class="tabs"><button data-ctab="leads" class="${CHK.tab==="leads"?"on":""}">Leads<span class="n">${leads.length}</span></button><button data-ctab="open" class="${CHK.tab==="open"?"on":""}">Open questions<span class="n">${open.length}</span></button>${hasChanges()?`<button data-ctab="changes" class="${CHK.tab==="changes"?"on":""}">Changes<span class="n">${chgItems().length}</span></button>`:""}</div>${body}`);
216
+ }
217
+ function chkLeadsHtml(leads){
218
+ if(!leads.length)return '<div class="empty">Nothing stood out in this capture. That says something about the capture, not the target.</div>';
219
+ const cnt=s=>leads.filter(x=>x.sev===s).length;
220
+ const shown=leads.map((x,i)=>[x,i]).filter(([x])=>CHK.sev==="all"||x.sev===CHK.sev);
221
+ return `<div class="fchips">${[["all","All",leads.length],["hot","High",cnt("hot")],["warn","Medium",cnt("warn")],["info","Note",cnt("info")]].filter(c=>c[2]||c[0]==="all").map(([k,l,n])=>`<button class="fchip${CHK.sev===k?" on":""}" data-csev="${k}">${l}<span class="n">${n}</span></button>`).join("")}</div>
222
+ <div id="leadlist">${leadGroups(shown,x=>x.lead,x=>x.sev).map(g=>g.items.length<2?chkLeadRow(g.items[0][0],g.items[0][1])
223
+ :`<div class="lgrp sev-${g.sev}"><div class="lgh"><span class="sevb">${{hot:"High",warn:"Medium",info:"Note"}[g.sev]}</span><b>${esc(g.lead.ttl)}</b><span class="n">${g.items.length}</span></div>
224
+ <div class="y">${esc(g.lead.why)}</div><div class="nx"><b>Do next</b>${esc(g.lead.next)}</div>
225
+ ${g.items.map(([x,i])=>chkLeadRowC(x,i)).join("")}</div>`).join("")||'<div class="empty">None at this level.</div>'}</div>`;
226
+ }
227
+ /* consecutive leads with the same reason and severity, so one explanation covers them all */
228
+ function leadGroups(pairs,leadOf,sevOf){
229
+ const out=[];
230
+ pairs.forEach(pr=>{const l=leadOf(pr[0]),s=sevOf(pr[0]),last=out[out.length-1];
231
+ if(last&&last.lead.ttl===l.ttl&&last.lead.why===l.why&&last.sev===s)last.items.push(pr);else out.push({lead:l,sev:s,items:[pr]});});
232
+ return out;
233
+ }
234
+ function chkLeadRowC(it,i){
235
+ const e=it.ep;
236
+ return `<div class="lrow c sev-${it.sev}${CHK.sel===i?" sel":""}" data-lead="${i}"><div class="rk">${i+1}</div><div><div class="t">${e?methodm(e.method)+`<span class="p">${esc(e.path)}</span>`:`<span class="p">cookie ${esc(it.cookie.name)}</span>`}${it.more.length?`<span class="more">+ ${it.more.map(s=>esc(s.ttl)).join(" · ")}</span>`:""}</div></div></div>`;
237
+ }
238
+ function chkOpenHtml(open){
239
+ if(!open.length)return '<div class="empty">Nothing was left open in this capture.</div>';
240
+ const types=tally(open,u=>u.type);
241
+ const shown=open.map((u,i)=>[u,i]).filter(([u])=>CHK.type==="all"||u.type===CHK.type);
242
+ const groups=new Map();shown.forEach(([u,i])=>{(groups.get(u.type)||groups.set(u.type,[]).get(u.type)).push([u,i]);});
243
+ return `<div class="fchips"><button class="fchip${CHK.type==="all"?" on":""}" data-ctype="all">All<span class="n">${open.length}</span></button>${types.map(([k,n])=>`<button class="fchip${CHK.type===k?" on":""}" data-ctype="${esc(k)}">${esc(openTitle(k))}<span class="n">${n}</span></button>`).join("")}</div>
244
+ <div id="openlist">${[...groups.entries()].map(([k,xs])=>{
245
+ /* when every question of a kind has the same unknown and the same next step, say it once */
246
+ const same=xs.length>1&&xs.every(([u])=>u.we_dont_know===xs[0][0].we_dont_know&&u.next===xs[0][0].next);
247
+ const tip=openTip(k,xs.length);
248
+ const head=same||tip?`<div class="qgh">${same?`<div class="un">Unknown for each: ${esc(xs[0][0].we_dont_know)}</div><div class="nx"><b>To find out</b>${esc(xs[0][0].next)}</div>`:""}${tip?`<div class="tip">${tip}</div>`:""}</div>`:"";
249
+ return `<div class="qgrp">${esc(openTitle(k))}<span class="n">${xs.length}</span></div>${head}${xs.slice(0,60).map(([u,i])=>`<div class="qrow${same?" c":""}${CHK.sel===i?" sel":""}" data-open="${i}"><div class="e">${esc(u.entity)}</div>
250
+ <ul>${u.we_know.map(x=>`<li>${esc(x)}</li>`).join("")}${same?"":`<li class="un">Unknown: ${esc(u.we_dont_know)}</li>`}</ul>${same?"":`<div class="nx"><b>To find out</b>${esc(u.next)}</div>`}</div>`).join("")}${xs.length>60?`<div class="more" style="color:var(--faint);font-size:12px">+ ${xs.length-60} more of this kind</div>`:""}`;}).join("")}</div>`;
251
+ }
252
+ /* the one move that answers a whole group of questions at once */
253
+ function openTip(type,n){
254
+ const roles=D.roles||[];
255
+ if(type==="AUTHORIZATION_UNKNOWN"&&roles.length<2)
256
+ return `<b>One capture answers all ${n}:</b> only ${roles.length?`the <span class="mono">${esc(roles[0])}</span> role is`:"one session is"} in this capture. Record a second account with <span class="mono">proxy2model browse</span> (it asks for a role), and the report compares what each role reached.`;
257
+ if(type==="API_PURPOSE_UNKNOWN"&&n>1)
258
+ return `Use the app's features that would call these while recording, or request them in an authorised session.`;
259
+ return "";
260
+ }
261
+ /* what moved since the previous build: workflows, changed fingerprints, new and gone */
262
+ const hasChanges=()=>!!(D.changes&&!D.changes.same);
263
+ function chgItems(){
264
+ const c=D.changes;if(!c)return [];const it=[];
265
+ (c.workflows||[]).forEach(w=>it.push({k:w.kind==="new_workflow"?"new workflow":"removed workflow",sev:w.kind==="new_workflow"?"hot":"info",title:w.title,sub:w.summary,chain:(w.chain||[]).join(" → "),ids:w.kind==="new_workflow"?w.evidence:[],gone:w.kind!=="new_workflow"}));
266
+ (c.changed||[]).forEach(x=>it.push({k:"changed",sev:"warn",title:x.label,sub:x.kind,lines:x.changes.map(y=>y.summary),ids:x.evidence||[]}));
267
+ ((c.new||{}).nodes||[]).forEach(x=>it.push({k:"new",sev:"info",title:x.label,sub:x.kind,ids:x.evidence||[]}));
268
+ ((c.new||{}).stack||[]).forEach(x=>it.push({k:"new",sev:"info",title:x.label,sub:x.category,ids:x.evidence||[]}));
269
+ ((c.new||{}).edges||[]).forEach(x=>it.push({k:"new",sev:"info",title:`${x.from} ${x.edge} ${x.to}`,sub:"link",ids:x.evidence||[]}));
270
+ ((c.gone||{}).nodes||[]).forEach(x=>it.push({k:"gone",sev:"info",title:x.label,sub:x.kind+(x.last_seen?` · last seen ${x.last_seen.slice(0,10)}`:""),ids:[],gone:true}));
271
+ ((c.gone||{}).stack||[]).forEach(x=>it.push({k:"gone",sev:"info",title:x.label,sub:x.category,ids:[],gone:true}));
272
+ ((c.gone||{}).edges||[]).forEach(x=>it.push({k:"gone",sev:"info",title:`${x.from} ${x.edge} ${x.to}`,sub:"link",ids:[],gone:true}));
273
+ return it;
274
+ }
275
+ function chkChangesHtml(){
276
+ const c=D.changes,s=c.summary,it=chgItems(),f=c.from||{},t=c.to||{};
277
+ const tag=m=>m&&m.id?`build ${m.id}${m.label?` (${esc(m.label)})`:m.built_at?` (${esc(m.built_at.slice(0,10))})`:""}`:"";
278
+ return `<p class="vsub" style="margin-top:-6px">${tag(f)} → ${tag(t)}: ${s.new_workflows} new workflow${s.new_workflows===1?"":"s"}, ${s.changed} changed, ${s.new} new, ${s.gone} gone. Connected additions are grouped into one line.</p>
279
+ <div id="chglist">${it.map((x,i)=>`<div class="lrow sev-${x.sev}${CHK.sel===i?" sel":""}" data-chg="${i}"><div class="rk">${i+1}</div><div>
280
+ <div class="t"><span class="sevb">${esc(x.k)}</span><span class="p">${esc(x.title)}</span></div>
281
+ ${x.sub?`<div class="y">${esc(x.sub)}</div>`:""}${x.chain?`<div class="w" style="font-weight:500">${esc(x.chain)}</div>`:""}
282
+ ${(x.lines||[]).map(l=>`<div class="w">${esc(l)}</div>`).join("")}</div></div>`).join("")}</div>`;
283
+ }
284
+ function initPriorities(){
285
+ splitInit();
286
+ const leads=chkLeads(),open=D.unknowns||[];
287
+ $$("[data-ctab]").forEach(b=>b.onclick=()=>{CHK.tab=b.dataset.ctab;CHK.sel=null;render();});
288
+ $$("[data-csev]").forEach(b=>b.onclick=()=>{CHK.sev=b.dataset.csev;CHK.sel=null;render();});
289
+ $$("[data-ctype]").forEach(b=>b.onclick=()=>{CHK.type=b.dataset.ctype;CHK.sel=null;render();});
290
+ const pick=(attr,list,show)=>$$(`[${attr}]`).forEach(r=>r.onclick=()=>{const i=+r.dataset[attr.replace("data-","")];CHK.sel=i;
291
+ $$(`[${attr}]`).forEach(x=>x.classList.toggle("sel",x===r));show(list[i]);});
292
+ pick("data-lead",leads,it=>splitShow(it.ids,{title:it.title,find:it.find,empty:{head:"No request for this one",text:"It was named in the code or the capture, but nothing was requested."}}));
293
+ pick("data-chg",chgItems(),it=>splitShow(it.ids,{title:it.title,empty:{head:it.gone?"No longer there":"No request of its own",text:it.gone?"The older build had this and the newer one does not, so there is nothing to open.":"This change comes from how things are linked, not from one request."}}));
294
+ pick("data-open",open,u=>{const ids=evForLabel(u.entity);splitShow(ids,{title:u.entity,empty:{head:"Nothing to open",text:"This question is about something the capture does not contain, so there is no request to show."}});});
295
+ const first=CHK.tab==="leads"?$("[data-lead]"):CHK.tab==="changes"?$("[data-chg]"):$("[data-open]");
296
+ if(first)first.click();
297
+ }
298
+
299
+ /* ---------- ENDPOINTS: the API in one table ---------- */
300
+ /* an endpoint only named in code has no method; its title is just the path on its host */
301
+ const epTitle=e=>(e.method&&e.method!=="*"?e.method+" ":"")+(e.host||"")+e.path;
302
+ const EPV={q:"",seen:"all",sort:"path",dir:1,sel:null};
303
+ const LEAD_OF=new Map(PRIO.map(p=>[p.ep.id,p]));
304
+ const SEEN={BOTH:["both","code + traffic"],RUNTIME_ONLY:["traffic","traffic only"],STATIC_ONLY:["code","code only"]};
305
+ const seenOf=e=>(SEEN[e.state]||["traffic","traffic only"])[0];
306
+ function epRows(){
307
+ const q=EPV.q.toLowerCase();
308
+ const sev=e=>{const p=LEAD_OF.get(e.id);return p?SEV_RANK[p.sev]:9;};
309
+ const key={path:e=>e.path+" "+e.method,requests:e=>-e.requests,params:e=>-e.params.length,status:e=>(e.statuses[0]||999),lead:e=>sev(e)}[EPV.sort]||(e=>e.path);
310
+ return EP.filter(e=>(EPV.seen==="all"||(EPV.seen==="lead"?LEAD_OF.has(e.id):EPV.seen==="admin"?e.privileged:seenOf(e)===EPV.seen))
311
+ &&(!q||(e.label+" "+(e.host||"")+" "+e.params.join(" ")+" "+e.roles.join(" ")).toLowerCase().includes(q)))
312
+ .sort((a,b)=>{const x=key(a),y=key(b);return (x<y?-1:x>y?1:0)*EPV.dir||a.path.localeCompare(b.path);});
313
+ }
314
+ function epTableHtml(){
315
+ const rows=epRows(),hosts=new Set(EP.map(e=>e.host)).size>1;
316
+ const th=(k,l)=>k?`<th data-epsort="${k}" class="srt${EPV.sort===k?" on":""}">${l}${EPV.sort===k?(EPV.dir>0?" ↑":" ↓"):""}</th>`:`<th>${l}</th>`;
317
+ const sevb=p=>p?`<span class="sevb sev-${p.sev}" title="${esc(p.sig[0].ttl)}">${{hot:"High",warn:"Medium",info:"Note"}[p.sev]}</span>`:"";
318
+ return `<div class="epscroll"><table class="dtable eptab"><thead><tr>${th("","")}${th("path","Endpoint")}${th("","Seen")}${th("status","Status")}${th("params","Params")}${th("","Credential")}${th("","Roles")}${th("requests","Requests")}${th("lead","To check")}${th("","")}</tr></thead><tbody>${
319
+ rows.slice(0,600).map(e=>`<tr data-eprow="${esc(e.id)}" class="${EPV.sel===e.id?"sel":""}"><td>${methodm(e.method)}</td>
320
+ <td><span class="mono">${esc(e.path)}</span>${e.privileged?' <span class="m w">admin</span>':""}${hosts?`<div class="dim mono sm">${esc(e.host||"")}</div>`:""}</td>
321
+ <td><span class="seen s-${seenOf(e)}">${esc((SEEN[e.state]||SEEN.RUNTIME_ONLY)[1])}</span></td><td>${statuses(e.statuses)}</td>
322
+ <td class="dim">${e.params.length||""}</td><td class="dim mono">${e.credentials.length?esc(e.credentials.join(", ")):e.requests?'<span title="No credential seen on any request">none seen</span>':""}</td>
323
+ <td class="dim mono">${esc(e.roles.join(", "))}</td><td class="dim">${e.requests||""}</td><td>${sevb(LEAD_OF.get(e.id))}</td>
324
+ <td><button class="rrbtn" data-epd="${esc(e.id)}">Details</button></td></tr>`).join("")
325
+ ||`<tr><td colspan="10"><div class="empty">No endpoint matches.</div></td></tr>`}</tbody></table></div>${rows.length>600?`<div class="dnote">Showing the first 600 of ${rows.length}. Filter to narrow.</div>`:""}`;
326
+ }
327
+ function vEndpoints(){
328
+ const n=k=>EP.filter(e=>seenOf(e)===k).length,leads=EP.filter(e=>LEAD_OF.has(e.id)).length,adm=EP.filter(e=>e.privileged).length;
329
+ const chips=[["all","All",EP.length],["traffic","Traffic only",n("traffic")],["both","Code + traffic",n("both")],["code","Code only",n("code")],["lead","To check",leads],["admin","Admin-looking",adm]]
330
+ .filter(c=>c[2]||c[0]==="all").map(([k,l,c])=>`<button class="fchip${EPV.seen===k?" on":""}" data-epseen="${k}">${l}<span class="n">${c}</span></button>`).join("");
331
+ return splitShell(`<h1 class="vt">Endpoints</h1><p class="vsub">The API this capture shows. <b>Traffic</b> means a request was captured; <b>code only</b> means a script names it but nothing called it. Pick a row to see its request below; <b>Details</b> shows parameters, roles and where it is named.</p>
332
+ <div class="dhead"><div class="fchips" style="margin:0">${chips}</div><input id="epq" placeholder="filter endpoints" title="Matches path, host, parameter names and roles" value="${esc(EPV.q)}"></div>
333
+ <div id="eptable">${epTableHtml()}</div>`);
334
+ }
335
+ function initEndpoints(){
336
+ splitInit();
337
+ const show=id=>{const e=EP.find(x=>x.id===id);if(!e)return;EPV.sel=id;
338
+ $$("tr[data-eprow]").forEach(r=>r.classList.toggle("sel",r.dataset.eprow===id));
339
+ splitShow(e.evidence,{title:epTitle(e),find:e.state==="STATIC_ONLY"?e.path:null,empty:{head:"Not requested",text:"The code names this endpoint but nothing in the capture called it."}});};
340
+ const wire=()=>{
341
+ $$("tr[data-eprow]").forEach(r=>r.onclick=()=>show(r.dataset.eprow));
342
+ $$("[data-epd]").forEach(b=>b.onclick=ev=>{ev.stopPropagation();openEp(b.dataset.epd);});
343
+ $$("[data-epsort]").forEach(t=>t.onclick=()=>{const k=t.dataset.epsort;if(EPV.sort===k)EPV.dir*=-1;else{EPV.sort=k;EPV.dir=1;}redraw();});
344
+ };
345
+ const redraw=()=>{$("#eptable").innerHTML=epTableHtml();wire();const r=$("tr.sel[data-eprow]")||$("tr[data-eprow]");if(r){show(r.dataset.eprow);r.scrollIntoView({block:"nearest"});}else splitShow([]);};
346
+ $$("[data-epseen]").forEach(b=>b.onclick=()=>{EPV.seen=b.dataset.epseen;EPV.sel=null;render();});
347
+ const qi=$("#epq");qi.oninput=()=>{EPV.q=qi.value;EPV.sel=null;redraw();};
348
+ redraw();
349
+ }
350
+
351
+ /* ---------- REFERENCE: the app in layers ---------- */
352
+ const REF={focusId:"root",pickId:null,rowId:null,q:""};
353
+ const REF_COL={pages:"var(--l2)",scripts:"var(--l3)",endpoints:"var(--l4)",parties:"var(--l5)",trust:"var(--signal)",infra:"var(--l1)"};
354
+ const REF_INFO={
355
+ root:["The whole app","Peel it layer by layer. Click a ring to open it; click the middle to go back out."],
356
+ pages:["Pages","The HTML documents the server returns. Each one loads scripts and calls endpoints."],
357
+ scripts:["Scripts","The JavaScript the browser runs. Code names more endpoints than the app ever calls."],
358
+ endpoints:["Endpoints","The API the app talks to, grouped by feature."],
359
+ parties:["Third parties","Code and services served from other domains, running in your users' browsers."],
360
+ trust:["Trust","How the app knows who you are: credentials, cookies and roles."],
361
+ infra:["Infrastructure","The hosts, servers and technologies underneath."],
362
+ auth:["Credentials","Schemes the app sent on requests."],cookies:["Cookies","Set by the server, with their protective flags."],
363
+ roles:["Roles","The accounts the capture was recorded as, and what each reached."],hosts:["Hosts","First-party hosts the app is served from."],stack:["Technology stack","Servers, frameworks and services read from the capture."]
364
+ };
365
+ let REF_TREE=null,REF_IDX=null;
366
+ function refBuild(){
367
+ const routeName=r=>r.label.replace(/^[^/]*/,"")||"/";
368
+ const areas=new Map();
369
+ EP.slice().sort((a,b)=>a.path.localeCompare(b.path)).forEach(e=>{const a=areaOf({type:"endpoint",label:e.label})||"root";(areas.get(a)||areas.set(a,[]).get(a)).push(e);});
370
+ const other=[];[...areas.entries()].forEach(([k,v])=>{if(v.length<2&&areas.size>6){other.push(...v);areas.delete(k);}});
371
+ if(other.length)areas.set("other",other);
372
+ const leaf=(kind,id,name,data,extra)=>Object.assign({kind,id,name,data,value:1},extra||{});
373
+ const nodes=[];
374
+ if((D.routes||[]).length)nodes.push({kind:"pages",id:"pages",name:"Pages",children:D.routes.map(r=>leaf("route","route:"+r.id,routeName(r),r))});
375
+ if((D.scripts||[]).length)nodes.push({kind:"scripts",id:"scripts",name:"Scripts",children:D.scripts.map(s=>leaf("script","script:"+s.id,s.label,s))});
376
+ if(EP.length)nodes.push({kind:"endpoints",id:"endpoints",name:"Endpoints",children:[...areas.entries()].sort((a,b)=>b[1].length-a[1].length||a[0].localeCompare(b[0])).map(([a,es])=>({kind:"area",id:"area:"+a,name:a,children:es.map(e=>leaf("endpoint","ep:"+e.id,(e.method!=="*"?e.method+" ":"")+e.path,e))}))});
377
+ if((D.third_parties||[]).length)nodes.push({kind:"parties",id:"parties",name:"Third parties",children:D.third_parties.map(t=>leaf("party","party:"+t.id,t.label,t))});
378
+ const tr=[];
379
+ if((D.auth||[]).length)tr.push({kind:"auth",id:"auth",name:"Credentials",children:D.auth.map(a=>leaf("authx","auth:"+a.id,a.label,a))});
380
+ if((D.cookies||[]).length)tr.push({kind:"cookies",id:"cookies",name:"Cookies",children:D.cookies.map(k=>leaf("cookie","cookie:"+k.name,k.name,k))});
381
+ if((D.roles||[]).length>0)tr.push({kind:"roles",id:"roles",name:"Roles",children:D.roles.map(r=>leaf("role","role:"+r,r,{label:r}))});
382
+ if(tr.length)nodes.push({kind:"trust",id:"trust",name:"Trust",children:tr});
383
+ const inf=[];
384
+ if((D.hosts||[]).length)inf.push({kind:"hosts",id:"hosts",name:"Hosts",children:D.hosts.map(h=>leaf("host","host:"+h.label,h.label,h))});
385
+ if((D.stack||[]).length)inf.push({kind:"stack",id:"stack",name:"Stack",children:(D.stack||[]).map(t=>leaf("tech","tech:"+t.category+":"+t.name,t.name+(t.version?" "+t.version:""),t))});
386
+ if(inf.length)nodes.push({kind:"infra",id:"infra",name:"Infrastructure",children:inf});
387
+ const root={kind:"root",id:"root",name:D.app,children:nodes};
388
+ REF_IDX=new Map();
389
+ const walk=(n,p)=>{n.parent=p;REF_IDX.set(n.id,n);if(n.children){n.children.forEach(c=>walk(c,n));n.value=n.children.reduce((a,c)=>a+c.value,0);}};
390
+ walk(root,null);
391
+ const tag=(n,t)=>{n.top=t;(n.children||[]).forEach(x=>tag(x,t));};
392
+ root.children.forEach(c=>tag(c,c.kind));root.top="root";
393
+ REF_TREE=root;
394
+ }
395
+ const refLeaves=n=>n.children?n.children.flatMap(refLeaves):[n];
396
+ function refCrumbs(n){const a=[];for(let x=n;x;x=x.parent)a.unshift(x);return a;}
397
+ function vReference(){
398
+ if(!REF_TREE)refBuild();
399
+ return splitShell(`<div class="refhd"><h1 class="vt">Reference</h1><p class="vsub">Peel the app in layers. Click a ring to go deeper, the middle to go back. The list on the right shows what you opened; its request is below.</p></div>
400
+ <div class="refgrid"><div class="refl"><div class="sunwrap" id="sunwrap"><svg viewBox="0 0 600 600" id="sun" class="sun" role="img" aria-label="The app in layers"></svg><div class="suntip" id="suntip"></div></div><div class="reft" id="reft"></div></div>
401
+ <div class="refr" id="refdetail"></div></div>`,"refpg");
402
+ }
403
+ function arcPath(cx,cy,a0,a1,r0,r1){
404
+ if(a1-a0>=Math.PI*2-1e-4)a1=a0+Math.PI*2-1e-4;
405
+ const P=(r,a)=>[cx+r*Math.sin(a),cy-r*Math.cos(a)],large=a1-a0>Math.PI?1:0,A=P(r1,a0),B=P(r1,a1),C=P(r0,a1),Dd=P(r0,a0);
406
+ return `M${A[0].toFixed(2)} ${A[1].toFixed(2)}A${r1} ${r1} 0 ${large} 1 ${B[0].toFixed(2)} ${B[1].toFixed(2)}L${C[0].toFixed(2)} ${C[1].toFixed(2)}A${r0} ${r0} 0 ${large} 0 ${Dd[0].toFixed(2)} ${Dd[1].toFixed(2)}Z`;
407
+ }
408
+ function initReference(){
409
+ splitInit();
410
+ if(!REF_TREE)refBuild();
411
+ if(!REF_IDX.has(REF.focusId))REF.focusId="root";
412
+ drawReference();
413
+ }
414
+ function drawReference(){
415
+ const focus=REF_IDX.get(REF.focusId)||REF_TREE,svg=$("#sun"),tip=$("#suntip");
416
+ const cx=300,cy=300,R0=74,RMAX=290;
417
+ const depthOf=n=>n.children?1+Math.max(...n.children.map(depthOf)):0;
418
+ const D3=Math.max(1,Math.min(3,depthOf(focus))),T=(RMAX-R0)/D3;
419
+ const col=n=>REF_COL[n.top]||"var(--l1)";
420
+ const arcs=[];
421
+ const place=(n,a0,a1,d)=>{
422
+ if(d>D3)return;
423
+ if(d>0)arcs.push({n,a0,a1,d});
424
+ if(!n.children||d>=D3)return;
425
+ const w=n.children.map(c=>Math.pow(c.value,.62)),tot=w.reduce((a,b)=>a+b,0);let a=a0;
426
+ n.children.forEach((c,i)=>{const span=(a1-a0)*w[i]/tot;place(c,a,a+span,d+1);a+=span;});
427
+ };
428
+ place(focus,0,Math.PI*2,0);
429
+ let h="";
430
+ arcs.forEach((x,i)=>{
431
+ const r0=R0+(x.d-1)*T,r1=r0+T-1.5,op=[1,.86,.7][x.d-1];
432
+ const pick=REF.pickId===x.n.id;
433
+ h+=`<path d="${arcPath(cx,cy,x.a0,x.a1,r0,r1)}" style="fill:${col(x.n)};opacity:${op}" class="${pick?"pick":""}" data-n="${esc(x.n.id)}"></path>`;
434
+ const mid=(x.a0+x.a1)/2,rm=(r0+r1)/2,len=(x.a1-x.a0)*rm;
435
+ if(len>15){
436
+ const deg=mid*180/Math.PI,flip=deg>180,rot=flip?deg+90:deg-90,tx=cx+rm*Math.sin(mid),ty=cy-rm*Math.cos(mid);
437
+ const max=Math.max(3,Math.floor((T-8)/6.3));let t=x.n.name||"";if(t.length>max)t=t.slice(0,max-1)+"…";
438
+ h+=`<text transform="translate(${tx.toFixed(1)} ${ty.toFixed(1)}) rotate(${rot.toFixed(1)})" text-anchor="middle" dominant-baseline="central">${esc(t)}</text>`;
439
+ }
440
+ });
441
+ const up=focus.parent;
442
+ h+=`<circle class="ctr" cx="${cx}" cy="${cy}" r="${R0-3}" data-up="1"></circle><text class="ct1" x="${cx}" y="${cy-2}">${esc((focus.name||"").slice(0,16))}</text><text class="ct2" x="${cx}" y="${cy+16}">${up?"click to go back":"click a ring"}</text>`;
443
+ svg.innerHTML=h;
444
+ const byId=id=>REF_IDX.get(id);
445
+ $$("path[data-n]",svg).forEach(p=>{
446
+ p.onclick=()=>{const n=byId(p.dataset.n);REF.pickId=n.id;REF.rowId=null;
447
+ if(n.children&&n.children.length)REF.focusId=n.id;else REF.focusId=(n.parent||REF_TREE).id;
448
+ if(!n.children&&n.kind==="endpoint")REF.rowId=n.data.id;else if(!n.children)REF.rowId=n.id;
449
+ drawReference();};
450
+ p.onpointermove=e=>{const n=byId(p.dataset.n),r=$("#sunwrap").getBoundingClientRect();tip.style.display="block";tip.style.left=Math.min(r.width-200,e.clientX-r.left+14)+"px";tip.style.top=(e.clientY-r.top+14)+"px";
451
+ tip.innerHTML=`<b>${esc(n.name)}</b><span>${esc(n.children?n.value+" item"+(n.value===1?"":"s"):humanize(n.kind))}</span>`;};
452
+ p.onpointerleave=()=>{tip.style.display="none";};
453
+ });
454
+ $(".ctr",svg).onclick=()=>{if(focus.parent){REF.focusId=focus.parent.id;REF.pickId=null;REF.rowId=null;drawReference();}};
455
+ refPanel(focus);refDetail(focus);
456
+ }
457
+ function refPanel(focus){
458
+ const info=REF_INFO[focus.kind]||[focus.name,""];
459
+ const crumbs=refCrumbs(focus);
460
+ const kids=focus.children||[];
461
+ $("#reft").innerHTML=`<div class="crumbs2">${crumbs.map((c,i)=>i<crumbs.length-1?`<button data-rc="${esc(c.id)}">${esc(c.name)}</button><span>/</span>`:`<span class="cur">${esc(c.name)}</span>`).join("")}</div>
462
+ <h2>${esc(focus.kind==="root"?D.app:info[0]&&focus.kind!=="area"?info[0]:focus.name)}</h2><p>${esc(focus.kind==="area"?`Endpoints under /${focus.name}.`:info[1])}</p>
463
+ ${kids.length?`<div class="refkids">${kids.slice(0,16).map(k=>`<button class="refkid" style="--c:${REF_COL[k.top]||"var(--l1)"}" data-rk="${esc(k.id)}"><i></i>${esc((k.name||"").slice(0,26))}<span class="n">${k.children?k.value:""}</span></button>`).join("")}${kids.length>16?`<span class="refkid" style="border-style:dashed">+${kids.length-16} more</span>`:""}</div>`:""}`;
464
+ $$("[data-rc],[data-rk]",$("#reft")).forEach(b=>b.onclick=()=>{const n=REF_IDX.get(b.dataset.rc||b.dataset.rk);if(!n)return;REF.pickId=null;REF.rowId=null;
465
+ if(n.children&&n.children.length)REF.focusId=n.id;else{REF.focusId=(n.parent||REF_TREE).id;REF.pickId=n.id;REF.rowId=n.kind==="endpoint"?n.data.id:n.id;}drawReference();});
466
+ }
467
+ /* what sits in the focused layer, as a table; a row opens its request below */
468
+ function refTable(cols,rows,rowId,selId){
469
+ return `<table class="dtable"><thead><tr>${cols.map(c=>`<th>${esc(c)}</th>`).join("")}</tr></thead><tbody>${rows.map(r=>`<tr data-row="${esc(rowId(r))}" class="${rowId(r)===selId?"sel":""}">${r.cells.map(c=>`<td>${c}</td>`).join("")}</tr>`).join("")||`<tr><td colspan="${cols.length}"><div class="empty">Nothing here.</div></td></tr>`}</tbody></table>`;
470
+ }
471
+ function refDetail(focus){
472
+ const box=$("#refdetail");let html="",onRow=null,pre=null;
473
+ const leaves=refLeaves(focus).filter(l=>l!==focus||!focus.children);
474
+ const of=k=>leaves.filter(l=>l.kind===k);
475
+ const k=focus.kind;
476
+ const sel=REF.rowId;
477
+ const info=(label,items,fn)=>items&&items.length?`<span class="k">${label}</span>${items.slice(0,14).map(fn||(x=>`<span class="c">${esc(x)}</span>`)).join("")}${items.length>14?`<span class="c">+${items.length-14}</span>`:""}`:"";
478
+ const jump=(label)=>{const e=EP.find(x=>x.label===label||(x.method+" "+x.path)===label||x.path===label);return e?`<button class="c" data-jump="${esc(e.id)}">${esc(label)}</button>`:`<span class="c">${esc(label)}</span>`;};
479
+ if(k==="root"){
480
+ html=`<div class="layers">${REF_TREE.children.map(c=>`<button class="layer" style="--c:${REF_COL[c.kind]||"var(--l1)"}" data-rk="${esc(c.id)}"><div class="nn">${refLeaves(c).length}</div><b>${esc(c.name)}</b><span>${esc((REF_INFO[c.kind]||["",""])[1])}</span></button>`).join("")}</div>`;
481
+ }else if(k==="pages"||k==="route"){
482
+ const rows=of("route").map(l=>({id:l.id,n:l,cells:[`<span class="mono">${esc(l.name)}</span>`,statuses(l.data.statuses),`<span class="dim">${l.data.calls.length} call${l.data.calls.length===1?"":"s"}</span>`,`<span class="dim">${l.data.scripts.length} script${l.data.scripts.length===1?"":"s"}</span>`,`<span class="dim mono">${esc((l.data.roles||[]).join(", ")||"")}</span>`]}));
483
+ html=`<div class="dhead"><h3>Pages</h3><span class="n">${rows.length}</span></div>${refTable(["Page","Status","Calls","Scripts","Roles"],rows,r=>r.id,sel)}<div class="pickinfo" id="pickinfo"></div>`;
484
+ onRow=id=>{const l=rows.find(r=>r.id===id).n;splitShow(l.data.evidence,{title:l.data.label});
485
+ $("#pickinfo").innerHTML=info("Calls",l.data.calls,jump)+info("Loads",l.data.scripts);};
486
+ }else if(k==="scripts"||k==="script"){
487
+ const rows=of("script").map(l=>({id:l.id,n:l,cells:[`<span class="mono">${esc(l.name)}</span>`,`<span class="dim mono">${esc(l.data.host||"")}</span>`,`<span class="dim">${l.data.references.length} endpoint reference${l.data.references.length===1?"":"s"}</span>`]}));
488
+ html=`<div class="dhead"><h3>Scripts</h3><span class="n">${rows.length}</span></div>${refTable(["Script","Host","References"],rows,r=>r.id,sel)}<div class="pickinfo" id="pickinfo"></div>`;
489
+ onRow=id=>{const l=rows.find(r=>r.id===id).n;splitShow(l.data.evidence,{title:l.data.label+" (beautified)"});
490
+ $("#pickinfo").innerHTML=info("Names these endpoints",l.data.references,jump);};
491
+ }else if(k==="endpoints"||k==="area"||k==="endpoint"){
492
+ const all=of("endpoint").map(l=>l.data),q=(REF.q||"").toLowerCase();
493
+ const list=all.filter(e=>!q||(e.label+" "+e.host).toLowerCase().includes(q));
494
+ const rows=list.slice(0,400).map(e=>({id:e.id,e,cells:[methodm(e.method),`<span class="mono">${esc(e.path)}</span>${e.privileged?' <span class="m w">admin</span>':""}`,
495
+ `<span class="dim">${e.state==="BOTH"?"code + traffic":e.state==="STATIC_ONLY"?"code only":"traffic only"}</span>`,statuses(e.statuses),`<span class="dim">${e.params.length||""}</span>`,`<span class="dim mono">${esc((e.roles||[]).join(", "))}</span>`]}));
496
+ html=`<div class="dhead"><h3>${esc(k==="area"?"/"+focus.name:"Endpoints")}</h3><span class="n">${list.length}${q?" of "+all.length:""}</span><input id="refq" placeholder="filter endpoints" value="${esc(REF.q)}"></div>${refTable(["","Endpoint","Seen","Status","Params","Roles"],rows,r=>r.id,sel)}${list.length>400?`<div class="dnote">Showing the first 400. Filter to narrow.</div>`:""}<div class="pickinfo" id="pickinfo"></div>`;
497
+ onRow=id=>{const e=EP.find(x=>x.id===id);splitShow(e.evidence,{title:e.label,find:e.state==="STATIC_ONLY"?e.path:null,empty:{head:"Not requested",text:"The code names this endpoint but nothing in the capture called it."}});
498
+ $("#pickinfo").innerHTML=info("Params",e.params)+info("Named in",e.referenced_by)+info("Called from",e.called_from)+info("Credentials",e.credentials)+info("Reached by",e.roles);};
499
+ }else if(k==="parties"||k==="party"){
500
+ const rows=of("party").map(l=>({id:l.id,n:l,cells:[`<span class="mono">${esc(l.name)}</span>`,`<span class="dim">${l.data.requests||(l.data.evidence||[]).length} request${(l.data.requests||(l.data.evidence||[]).length)===1?"":"s"}</span>`,`<span class="dim mono">${esc((l.data.referenced_by||[]).join(", "))}</span>`]}));
501
+ html=`<div class="dhead"><h3>Third parties</h3><span class="n">${rows.length}</span></div>${refTable(["Host","Requests","Named in"],rows,r=>r.id,sel)}`;
502
+ onRow=id=>{const l=rows.find(r=>r.id===id).n;splitShow(l.data.evidence,{title:l.data.label});};
503
+ }else if(k==="trust"||k==="auth"||k==="cookies"){
504
+ const au=k==="trust"||k==="auth"?of("authx"):[],ck=k==="trust"||k==="cookies"?of("cookie"):[];
505
+ const yes=v=>v?'<span class="st g">yes</span>':'<span class="st r">no</span>';
506
+ const ar=au.map(l=>({id:l.id,n:l,cells:[`<span class="mono">${esc(l.name)}</span>`,`<span class="dim">${l.data.endpoints.length} endpoint${l.data.endpoints.length===1?"":"s"}</span>`]}));
507
+ const cr=ck.map(l=>({id:l.id,n:l,cells:[`<span class="mono">${esc(l.name)}</span>`,yes(l.data.httponly),yes(l.data.secure),`<span class="mono dim">${esc(l.data.samesite||"unset")}</span>`]}));
508
+ html=(ar.length?`<div class="dsec"><div class="dhead"><h3>Credentials</h3><span class="n">${ar.length}</span></div>${refTable(["Scheme","Seen on"],ar,r=>r.id,sel)}</div>`:"")+
509
+ (cr.length?`<div class="dsec"><div class="dhead"><h3>Cookies</h3><span class="n">${cr.length}</span></div>${refTable(["Cookie","HttpOnly","Secure","SameSite"],cr,r=>r.id,sel)}</div>`:"")+
510
+ (k==="trust"&&(D.roles||[]).length?`<div class="pickinfo"><span class="k">Roles</span>${D.roles.map(r=>`<button class="c" data-rk="roles">${esc(r)}</button>`).join("")}</div>`:"")+
511
+ (!ar.length&&!cr.length?'<div class="empty">No credentials or cookies were observed.</div>':"")+`<div class="pickinfo" id="pickinfo"></div>`;
512
+ onRow=id=>{const r=ar.concat(cr).find(x=>x.id===id);if(!r)return;let ids=r.n.data.evidence||[];
513
+ if(r.n.kind==="authx"){const e=EP.find(x=>r.n.data.endpoints.includes(x.label));ids=e?e.evidence:[];}
514
+ splitShow(ids,{title:r.n.name});
515
+ $("#pickinfo").innerHTML=r.n.kind==="authx"?info("Seen on",r.n.data.endpoints,jump):"";};
516
+ }else if(k==="roles"||k==="role"){
517
+ html=`<div id="xrole">${vCrossrole().replace(/<h1[^>]*>.*?<\/h1>/,"").replace(/<p class="vsub">.*?<\/p>/,"")}</div>`;
518
+ }else if(k==="infra"||k==="hosts"||k==="stack"||k==="host"||k==="tech"){
519
+ const hs=(D.hosts||[]);
520
+ const hrows=hs.map(h=>({id:"host:"+h.label,h,cells:[`<span class="mono">${esc(h.label)}</span>`,`<span class="dim mono">${esc((h.schemes||[]).join("/")+(h.ports&&h.ports.length?":"+h.ports.join(","):""))}</span>`,`<span class="dim mono">${esc(Object.entries(h.tech||{}).map(([a,b])=>b?a+": "+b:a).join(" · ")||"")}</span>`]}));
521
+ const sc=k==="stack"||k==="infra"||k==="tech"?stackCard():"",ic=k==="infra"||k==="hosts"||k==="host"?infraCard():"";
522
+ html=(k!=="stack"&&k!=="tech"?`<div class="dsec"><div class="dhead"><h3>Hosts</h3><span class="n">${hs.length}</span></div>${refTable(["Host","Served over","Headers"],hrows,r=>r.id,sel)}</div>`:"")+
523
+ `<div class="stackrow">${ic}${sc}</div>`+(k!=="stack"&&k!=="tech"&&BQLDB&&BQLDB.osint?`<div class="dsec"><div class="dhead"><h3>External recon</h3></div>${vInfra().replace(/<h1[^>]*>.*?<\/h1>/,"").replace(/<p class="vsub">[\s\S]*?<\/p>/,"")}</div>`:"")+`<div class="pickinfo" id="pickinfo"></div>`;
524
+ onRow=id=>{const r=hrows.find(x=>x.id===id);if(r)splitShow(r.h.evidence,{title:r.h.label});};
525
+ }
526
+ box.innerHTML=html;
527
+ const qi=$("#refq");if(qi)qi.oninput=()=>{REF.q=qi.value;const p=qi.selectionStart;refDetail(focus);const n=$("#refq");n.focus();n.setSelectionRange(p,p);};
528
+ $$("[data-rk]",box).forEach(b=>b.onclick=()=>{const n=REF_IDX.get(b.dataset.rk);if(n){REF.focusId=n.id;REF.pickId=null;REF.rowId=null;drawReference();}});
529
+ $$("[data-jump]",box).forEach(b=>b.onclick=()=>{REF.focusId="endpoints";REF.pickId="ep:"+b.dataset.jump;REF.rowId=b.dataset.jump;REF.q="";drawReference();});
530
+ $$("[data-ev]",box).forEach(b=>b.onclick=()=>openEv(b.dataset.ev));
531
+ $$("[data-ep]",box).forEach(b=>b.onclick=()=>openEp(b.dataset.ep));
532
+ $$("tr[data-row]",box).forEach(r=>r.onclick=()=>{REF.rowId=r.dataset.row;$$("tr[data-row]",box).forEach(x=>x.classList.toggle("sel",x===r));if(onRow)onRow(r.dataset.row);});
533
+ if(onRow&&sel&&$(`tr[data-row="${CSS.escape(sel)}"]`,box)){const tr=$(`tr[data-row="${CSS.escape(sel)}"]`,box);tr.classList.add("sel");onRow(sel);tr.scrollIntoView({block:"nearest"});}
534
+ else if(onRow){const f=$("tr[data-row]",box);if(f)f.click();else splitShow([]);}
535
+ else splitShow([]);
536
+ }
537
+ """