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,469 @@
|
|
|
1
|
+
"""The Dashboard view of the offline report: charts and rankings drawn as plain SVG/CSS.
|
|
2
|
+
|
|
3
|
+
Kept apart from report.py so the template stays readable. `DASH_CSS` and `DASH_JS` are
|
|
4
|
+
spliced into the page; everything is computed from the payload `D` the report embeds.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
DASH_CSS = r"""
|
|
8
|
+
/* dashboard */
|
|
9
|
+
/* at a glance: screenshot, infrastructure, stack */
|
|
10
|
+
.band{display:grid;gap:18px;align-items:start;margin-bottom:24px;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)}
|
|
11
|
+
.band.shot{grid-template-columns:minmax(0,.95fr) minmax(0,.9fr) minmax(0,1.3fr)}
|
|
12
|
+
@media(max-width:1250px){.band.shot{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.band.shot .shotcard{grid-column:1/-1}}
|
|
13
|
+
@media(max-width:900px){.band,.band.shot{grid-template-columns:minmax(0,1fr)}}
|
|
14
|
+
.shotcard{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;display:flex;flex-direction:column}
|
|
15
|
+
.shotcard .sbar{display:flex;align-items:center;gap:6px;padding:9px 12px;background:var(--panel2);border-bottom:1px solid var(--line)}
|
|
16
|
+
.shotcard .sbar i{width:9px;height:9px;border-radius:50%;background:var(--line2)}
|
|
17
|
+
.shotcard .sbar span{margin-left:8px;flex:1;font:11.5px var(--mono);color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:3px 9px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
18
|
+
.shotcard button.simg{border:0;padding:0;background:var(--ink);cursor:zoom-in;flex:1;min-height:200px;display:block}
|
|
19
|
+
.shotcard img{width:100%;height:100%;min-height:200px;max-height:340px;object-fit:cover;object-position:top;display:block}
|
|
20
|
+
.lightbox{position:fixed;inset:0;z-index:80;background:rgba(10,13,18,.82);display:grid;place-items:center;padding:30px;cursor:zoom-out}
|
|
21
|
+
.lightbox img{max-width:100%;max-height:100%;border-radius:10px;box-shadow:var(--shadow)}
|
|
22
|
+
.irows{display:grid;grid-template-columns:104px minmax(0,1fr);gap:0}
|
|
23
|
+
.irows .k{color:var(--muted);font-size:12.5px;padding:6px 0;border-bottom:1px solid var(--line)}
|
|
24
|
+
.irows .v{font:12.5px var(--mono);padding:6px 0;border-bottom:1px solid var(--line);overflow-wrap:anywhere}
|
|
25
|
+
.irows .k:nth-last-child(2),.irows .v:last-child{border-bottom:0}
|
|
26
|
+
.irows .v small{color:var(--faint);margin-left:6px}
|
|
27
|
+
.smap{display:flex;align-items:stretch;gap:0;flex-wrap:wrap;row-gap:12px}
|
|
28
|
+
.smap .sn{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:8px 10px;min-width:96px;display:flex;flex-direction:column;gap:5px;align-self:stretch}
|
|
29
|
+
.smap .sn.you{justify-content:center;align-items:center;font:600 12px var(--mono);color:var(--muted);min-width:70px}
|
|
30
|
+
.smap .sl{font:600 9.5px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
|
31
|
+
.smap .sa{display:grid;place-items:center;width:24px;color:var(--faint);font-size:15px}
|
|
32
|
+
.tp{display:inline-flex;align-items:center;gap:6px;font:600 12px var(--sans);padding:3px 4px 3px 0;border:0;background:none;color:var(--text);text-align:left}
|
|
33
|
+
.tp i{width:8px;height:8px;border-radius:2px;flex:none}
|
|
34
|
+
.tp small{color:var(--faint);font:500 10.5px var(--mono)}
|
|
35
|
+
.tp:hover{color:var(--signal)}
|
|
36
|
+
.svc{margin-top:14px;display:grid;grid-template-columns:96px minmax(0,1fr);gap:8px 12px;align-items:baseline}
|
|
37
|
+
.svc .sk{font:600 10px var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
|
|
38
|
+
.svc .sv{display:flex;flex-wrap:wrap;gap:4px 14px}
|
|
39
|
+
.chgbar{display:flex;gap:18px;align-items:center;flex-wrap:wrap;width:100%;text-align:left;margin-bottom:18px;padding:12px 16px;border:1px solid var(--line);border-left:4px solid var(--observed);border-radius:12px;background:var(--panel);font:13px var(--sans);color:var(--muted)}
|
|
40
|
+
.chgbar b{color:var(--text);font-weight:700}.chgbar i{margin-left:auto;color:var(--signal);font-style:normal;font-weight:600}
|
|
41
|
+
.chgbar:hover{border-color:var(--line2);border-left-color:var(--observed)}
|
|
42
|
+
.m-overview{padding:26px 32px 60px}
|
|
43
|
+
.ovh{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:28px;align-items:end;margin-bottom:26px}
|
|
44
|
+
@media(max-width:1000px){.ovh{grid-template-columns:minmax(0,1fr)}}
|
|
45
|
+
.ovh h1{font:700 30px/1.1 var(--sans);letter-spacing:-.025em;margin:0 0 8px}
|
|
46
|
+
.ovh .sum{font-size:16px;line-height:1.55;color:var(--muted);max-width:62ch;margin:0}
|
|
47
|
+
.ovh .sum b{color:var(--text);font-weight:700}
|
|
48
|
+
.ovh .meta{color:var(--faint);font:12px var(--mono);margin-top:10px}
|
|
49
|
+
.big4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
|
|
50
|
+
.big4 button{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 14px 12px;text-align:left;cursor:pointer}
|
|
51
|
+
.big4 button:hover{border-color:var(--line2);box-shadow:var(--shadow)}
|
|
52
|
+
.big4 .n{font:700 32px/1 var(--sans);letter-spacing:-.03em}
|
|
53
|
+
.big4 .k{font-size:12px;color:var(--muted);margin-top:6px}
|
|
54
|
+
.big4 .hot .n{color:var(--signal)}
|
|
55
|
+
.ovg{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);gap:18px;margin-bottom:22px}
|
|
56
|
+
@media(max-width:1100px){.ovg{grid-template-columns:minmax(0,1fr)}}
|
|
57
|
+
.sech{display:flex;align-items:baseline;gap:10px;margin:0 0 12px}
|
|
58
|
+
.sech h2{font:700 16px var(--sans);margin:0;letter-spacing:-.01em}
|
|
59
|
+
.sech .s{color:var(--faint);font-size:12.5px}
|
|
60
|
+
.sech .lnk{margin-left:auto;font:600 12.5px var(--sans);color:var(--signal);background:none;border:0}
|
|
61
|
+
.lead{display:grid;grid-template-columns:30px minmax(0,1fr) auto;gap:14px;align-items:start;background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--c,var(--signal));border-radius:12px;padding:14px 16px;margin-bottom:10px}
|
|
62
|
+
.lead .num{font:700 14px var(--mono);color:var(--c,var(--signal));padding-top:2px}
|
|
63
|
+
.lead .t{display:flex;gap:8px;align-items:center;flex-wrap:wrap;font:700 14px var(--mono);word-break:break-all}
|
|
64
|
+
.lead .w{font:600 13px var(--sans);margin-top:4px}
|
|
65
|
+
.lead .y{color:var(--muted);font-size:12.5px;margin-top:2px;max-width:80ch}
|
|
66
|
+
.lead .nx{font-size:12.5px;margin-top:6px;color:var(--text)}
|
|
67
|
+
.lead .nx b{font:700 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--c,var(--signal));margin-right:6px}
|
|
68
|
+
.lead .acts{display:flex;flex-direction:column;gap:6px}
|
|
69
|
+
.abtn{font:600 12px var(--sans);padding:6px 11px;border-radius:8px;border:1px solid var(--line2);background:var(--panel);color:var(--muted);white-space:nowrap}
|
|
70
|
+
.abtn:hover{color:var(--text);border-color:var(--faint)}
|
|
71
|
+
.abtn.pri{background:var(--text);color:var(--ink);border-color:var(--text)}
|
|
72
|
+
.lead .eps{display:flex;flex-direction:column;gap:2px;margin-top:8px}
|
|
73
|
+
.lead .eps button{display:flex;gap:8px;align-items:center;border:0;background:none;padding:4px 6px;margin:0 -6px;border-radius:6px;text-align:left;font:600 12.5px var(--mono);color:var(--text);word-break:break-all}
|
|
74
|
+
.lead .eps button:hover{background:var(--panel2);color:var(--signal)}
|
|
75
|
+
.lead .eps .more{font:12px var(--sans);color:var(--faint);padding:2px 0}
|
|
76
|
+
@media(max-width:700px){.lead{grid-template-columns:24px minmax(0,1fr)}.lead .acts{grid-column:2;flex-direction:row}}
|
|
77
|
+
.side{display:flex;flex-direction:column;gap:14px}
|
|
78
|
+
.tiles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:22px}
|
|
79
|
+
@media(max-width:1000px){.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
80
|
+
.tile{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px;cursor:pointer;text-align:left}
|
|
81
|
+
.tile:hover{border-color:var(--signal);box-shadow:var(--shadow)}
|
|
82
|
+
.tile b{display:block;font:700 14.5px var(--sans);margin-bottom:4px}
|
|
83
|
+
.tile span{color:var(--muted);font-size:12.5px}
|
|
84
|
+
details.more{border:1px solid var(--line);border-radius:14px;background:var(--panel);margin-bottom:18px}
|
|
85
|
+
details.more>summary{cursor:pointer;list-style:none;padding:14px 18px;font:700 14px var(--sans);display:flex;gap:10px;align-items:center}
|
|
86
|
+
details.more>summary::-webkit-details-marker{display:none}
|
|
87
|
+
details.more>summary::before{content:"▸";color:var(--faint);transition:transform .15s}
|
|
88
|
+
details.more[open]>summary::before{transform:rotate(90deg)}
|
|
89
|
+
details.more>summary span{font-weight:500;color:var(--faint);font-size:12.5px}
|
|
90
|
+
details.more .inner{padding:0 18px 18px}
|
|
91
|
+
.dh1{display:flex;align-items:flex-end;gap:18px;flex-wrap:wrap;margin-bottom:18px}
|
|
92
|
+
.dh1 h1{font:700 28px/1.1 var(--sans);letter-spacing:-.025em;margin:0}
|
|
93
|
+
.dh1 .sub{color:var(--muted);font:12.5px var(--mono);margin-top:6px}
|
|
94
|
+
.dh1 .sp{flex:1}
|
|
95
|
+
.dtags{display:flex;gap:6px;flex-wrap:wrap}
|
|
96
|
+
.dtag{font:600 11px var(--mono);padding:4px 10px;border-radius:999px;background:var(--panel);border:1px solid var(--line);color:var(--muted)}
|
|
97
|
+
.dtag b{color:var(--text)}
|
|
98
|
+
.dkpis{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin-bottom:14px}
|
|
99
|
+
@media(max-width:1180px){.dkpis{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
|
100
|
+
@media(max-width:620px){.dkpis{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
101
|
+
.dk{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:15px 16px 13px;overflow:hidden;cursor:pointer;
|
|
102
|
+
transition:transform .12s,border-color .12s,box-shadow .12s}
|
|
103
|
+
.dk:hover{transform:translateY(-2px);border-color:var(--line2);box-shadow:var(--shadow)}
|
|
104
|
+
.dk::before{content:"";position:absolute;inset:0 0 auto 0;height:3px;background:var(--c,var(--line2))}
|
|
105
|
+
.dk .n{font:700 32px/1 var(--sans);letter-spacing:-.03em}
|
|
106
|
+
.dk .k{font:600 11px var(--mono);letter-spacing:.07em;text-transform:uppercase;color:var(--muted);margin-top:8px}
|
|
107
|
+
.dk .s{color:var(--faint);font-size:11.5px;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
108
|
+
.dgrid{display:grid;gap:14px;margin-bottom:14px}
|
|
109
|
+
.dg3{grid-template-columns:repeat(3,minmax(0,1fr))}
|
|
110
|
+
.dg2{grid-template-columns:minmax(0,1.35fr) minmax(0,1fr)}
|
|
111
|
+
.dg21{grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)}
|
|
112
|
+
@media(max-width:1100px){.dg3,.dg2,.dg21{grid-template-columns:minmax(0,1fr)}}
|
|
113
|
+
.dc{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;display:flex;flex-direction:column;min-width:0}
|
|
114
|
+
.dc>.h{display:flex;align-items:center;gap:8px;padding:13px 16px 0;font:600 13px var(--sans)}
|
|
115
|
+
.dc>.h .ct{margin-left:auto;color:var(--faint);font:600 11px var(--mono)}
|
|
116
|
+
.dc>.h .lnk{margin-left:auto;font:600 12px var(--sans);color:var(--signal);background:none;border:0;padding:0}
|
|
117
|
+
.dc>.b{padding:14px 16px 16px;flex:1;min-width:0}
|
|
118
|
+
.dnote{color:var(--faint);font-size:11.5px;margin-top:10px}
|
|
119
|
+
/* donut */
|
|
120
|
+
.dwrap{display:flex;align-items:center;gap:20px;flex-wrap:wrap}
|
|
121
|
+
.donut text{font-family:var(--sans)}
|
|
122
|
+
.dleg{display:flex;flex-direction:column;gap:9px;min-width:150px;flex:1}
|
|
123
|
+
.dleg .r{display:flex;align-items:center;gap:8px;font-size:12.5px;cursor:pointer}
|
|
124
|
+
.dleg .r:hover b{color:var(--signal)}
|
|
125
|
+
.dleg .sw{width:10px;height:10px;border-radius:3px;flex:none}
|
|
126
|
+
.dleg .l{color:var(--muted);flex:1}
|
|
127
|
+
.dleg b{font:600 13px var(--mono)}
|
|
128
|
+
.dleg .pc{color:var(--faint);font:11px var(--mono);width:38px;text-align:right}
|
|
129
|
+
/* bars */
|
|
130
|
+
.hb{display:flex;flex-direction:column;gap:8px}
|
|
131
|
+
.hb .r{display:grid;grid-template-columns:minmax(80px,38%) minmax(0,1fr) 44px;align-items:center;gap:10px;font-size:12.5px;cursor:pointer}
|
|
132
|
+
.hb .r .l{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-family:var(--mono);font-size:12px}
|
|
133
|
+
.hb .r .t{height:9px;border-radius:999px;background:var(--panel2);overflow:hidden}
|
|
134
|
+
.hb .r .t i{display:block;height:100%;border-radius:999px;background:var(--c,var(--observed));transform-origin:left;animation:grow .7s cubic-bezier(.2,.8,.2,1) both}
|
|
135
|
+
.hb .r .v{font:600 12px var(--mono);color:var(--muted);text-align:right}
|
|
136
|
+
.hb .r:hover .l{color:var(--signal)}
|
|
137
|
+
@keyframes grow{from{transform:scaleX(0)}}
|
|
138
|
+
@media(prefers-reduced-motion:reduce){.hb .r .t i{animation:none}.view.on{animation:none}}
|
|
139
|
+
.hb.sm .r{grid-template-columns:54px minmax(0,1fr) 44px}
|
|
140
|
+
.hb.wide .r{grid-template-columns:minmax(110px,52%) minmax(0,1fr) 40px}
|
|
141
|
+
.hb.wide .r .l{font-family:var(--sans);font-size:12.5px}
|
|
142
|
+
/* stacked */
|
|
143
|
+
.stk{display:flex;height:14px;border-radius:999px;overflow:hidden;background:var(--panel2)}
|
|
144
|
+
.stk i{display:block;height:100%}
|
|
145
|
+
.stkleg{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px;font-size:12px;color:var(--muted)}
|
|
146
|
+
.stkleg .k{display:inline-flex;gap:6px;align-items:center}
|
|
147
|
+
.stkleg .sw{width:9px;height:9px;border-radius:3px}
|
|
148
|
+
.stkleg b{font:600 12px var(--mono);color:var(--text)}
|
|
149
|
+
/* attention */
|
|
150
|
+
.att{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
151
|
+
.att .a{border:1px solid var(--line);border-radius:12px;padding:12px 12px 10px;cursor:pointer;background:var(--panel2);border-top:3px solid var(--c)}
|
|
152
|
+
.att .a:hover{border-color:var(--c)}
|
|
153
|
+
.att .a .n{font:700 28px/1 var(--sans);color:var(--c)}
|
|
154
|
+
.att .a .k{font:600 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-top:6px}
|
|
155
|
+
.dlist .it{display:flex;align-items:flex-start;gap:10px;padding:10px 0;border-bottom:1px solid var(--line);cursor:pointer}
|
|
156
|
+
.dlist .it:last-child{border-bottom:0;padding-bottom:0}
|
|
157
|
+
.dlist .it:first-child{padding-top:0}
|
|
158
|
+
.dlist .rk{font:700 12px var(--mono);color:var(--c,var(--signal));width:18px;flex:none;padding-top:2px}
|
|
159
|
+
.dlist .tx{min-width:0;flex:1}
|
|
160
|
+
.dlist .tt{display:flex;gap:8px;align-items:center;flex-wrap:wrap;font:600 13px var(--mono)}
|
|
161
|
+
.dlist .tt .p{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
|
|
162
|
+
.dlist .ds{color:var(--muted);font-size:12px;margin-top:2px}
|
|
163
|
+
.dlist .it:hover .tt .p{color:var(--signal)}
|
|
164
|
+
.sevd{width:8px;height:8px;border-radius:50%;background:var(--c);flex:none;margin-top:6px}
|
|
165
|
+
/* layer strip */
|
|
166
|
+
.lstrip{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:0;border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--panel);margin-bottom:14px}
|
|
167
|
+
@media(max-width:900px){.lstrip{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
168
|
+
.ls{padding:14px 16px;border-right:1px solid var(--line);position:relative;cursor:pointer}
|
|
169
|
+
.ls:last-child{border-right:0}
|
|
170
|
+
.ls:hover{background:var(--panel2)}
|
|
171
|
+
.ls .ln{font:600 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--c);display:flex;align-items:center;gap:6px}
|
|
172
|
+
.ls .ln::before{content:"";width:8px;height:8px;border-radius:2px;background:var(--c)}
|
|
173
|
+
.ls .n{font:700 24px/1.1 var(--sans);margin-top:8px;letter-spacing:-.02em}
|
|
174
|
+
.ls .d{color:var(--faint);font-size:11.5px;margin-top:3px}
|
|
175
|
+
/* capture strip */
|
|
176
|
+
.cstrip{width:100%;height:56px;display:block}
|
|
177
|
+
.cstrip rect{shape-rendering:crispEdges}
|
|
178
|
+
.rolebar{display:flex;margin-top:6px;height:22px;border-radius:6px;overflow:hidden;font:600 10.5px var(--mono)}
|
|
179
|
+
.rolebar span{display:flex;align-items:center;padding:0 7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#fff;min-width:0}
|
|
180
|
+
.dtbl{width:100%;font-size:12.5px}
|
|
181
|
+
.dtbl td{padding:7px 0;border-bottom:1px solid var(--line)}
|
|
182
|
+
.dtbl tr:last-child td{border-bottom:0}
|
|
183
|
+
.dtbl td:last-child{text-align:right;font-family:var(--mono);color:var(--muted)}
|
|
184
|
+
.dtbl tr{cursor:pointer}.dtbl tbody tr:hover{background:none}.dtbl tr:hover td:first-child{color:var(--signal)}
|
|
185
|
+
.okt{color:var(--ok)}.badt{color:var(--bad)}.wrnt{color:var(--warn)}
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
DASH_JS = r"""
|
|
189
|
+
/* ---------- DASHBOARD ---------- */
|
|
190
|
+
const EVL=Object.values(D.evidence||{}).sort((a,b)=>a.id-b.id);
|
|
191
|
+
const fmtN=n=>n>=10000?Math.round(n/1000)+"k":n>=1000?(n/1000).toFixed(1)+"k":String(n);
|
|
192
|
+
const pct=(a,b)=>b?Math.round(a/b*100)+"%":"0%";
|
|
193
|
+
function tally(xs,f){const m=new Map();xs.forEach(x=>{const k=f(x);if(k!=null)m.set(k,(m.get(k)||0)+1);});return [...m.entries()].sort((a,b)=>b[1]-a[1]||String(a[0]).localeCompare(String(b[0])));}
|
|
194
|
+
const statusClass=s=>s==null?"?":s>=500?"5xx":s>=400?"4xx":s>=300?"3xx":s>=200?"2xx":"1xx";
|
|
195
|
+
const SC_COL={"2xx":"var(--ok)","3xx":"var(--observed)","4xx":"var(--warn)","5xx":"var(--bad)","?":"var(--faint)","1xx":"var(--faint)"};
|
|
196
|
+
function donut(parts,size,thick,big,small){
|
|
197
|
+
const tot=parts.reduce((a,p)=>a+p.v,0)||1,r=size/2-thick/2,C=2*Math.PI*r,h=size/2;let off=0;
|
|
198
|
+
const arcs=parts.filter(p=>p.v).map(p=>{const len=C*p.v/tot;const gap=parts.filter(q=>q.v).length>1?2:0;
|
|
199
|
+
const s=`<circle r="${r}" cx="${h}" cy="${h}" fill="none" stroke="${p.color}" stroke-width="${thick}" stroke-dasharray="${Math.max(0,len-gap)} ${C-len+gap}" stroke-dashoffset="${-off}" transform="rotate(-90 ${h} ${h})"><title>${esc(p.label)}: ${p.v}</title></circle>`;off+=len;return s;}).join("");
|
|
200
|
+
return `<svg class="donut" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" role="img"><circle r="${r}" cx="${h}" cy="${h}" fill="none" stroke="var(--panel2)" stroke-width="${thick}"/>${arcs}
|
|
201
|
+
<text x="${h}" y="${h+2}" text-anchor="middle" font-size="${size*.2}" font-weight="700" fill="var(--text)">${esc(big)}</text>
|
|
202
|
+
<text x="${h}" y="${h+size*.12}" text-anchor="middle" font-size="${size*.075}" font-weight="600" fill="var(--muted)" letter-spacing=".08em">${esc(small)}</text></svg>`;
|
|
203
|
+
}
|
|
204
|
+
function hbars(rows,opt){
|
|
205
|
+
opt=opt||{};const mx=Math.max(1,...rows.map(r=>r.v));
|
|
206
|
+
return `<div class="hb${opt.sm?" sm":""}${opt.wide?" wide":""}">${rows.map(r=>`<div class="r" ${r.attr||""} title="${esc(r.title||r.label)}"><span class="l">${esc(r.label)}</span><span class="t"><i style="width:${Math.max(2,r.v/mx*100).toFixed(1)}%;--c:${r.color||opt.color||"var(--observed)"}"></i></span><span class="v">${r.vtxt!=null?r.vtxt:fmtN(r.v)}</span></div>`).join("")}</div>`;
|
|
207
|
+
}
|
|
208
|
+
function stacked(parts){
|
|
209
|
+
const tot=parts.reduce((a,p)=>a+p.v,0)||1;
|
|
210
|
+
return `<div class="stk">${parts.filter(p=>p.v).map(p=>`<i style="width:${(p.v/tot*100).toFixed(2)}%;background:${p.color}" title="${esc(p.label)}: ${p.v}"></i>`).join("")}</div>
|
|
211
|
+
<div class="stkleg">${parts.filter(p=>p.v).map(p=>`<span class="k"><span class="sw" style="background:${p.color}"></span>${esc(p.label)} <b>${fmtN(p.v)}</b></span>`).join("")}</div>`;
|
|
212
|
+
}
|
|
213
|
+
function provenanceBar(){return "";}
|
|
214
|
+
function hostsCard(){return "";}
|
|
215
|
+
function osintCard(){
|
|
216
|
+
const o=D.osint;if(!o)return "";
|
|
217
|
+
const rows=[];
|
|
218
|
+
if(o.tls&&o.tls.issuer)rows.push(["TLS",`${o.tls.issuer}${o.tls.not_after?` · expires ${o.tls.not_after}`:""}`]);
|
|
219
|
+
if(o.hosting&&o.hosting.network)rows.push(["Hosting",`${o.hosting.network}${o.hosting.country?` · ${o.hosting.country}`:""}`]);
|
|
220
|
+
if((o.technology||[]).length)rows.push(["Technology",o.technology.join(", ")]);
|
|
221
|
+
if(o.headers_missing)rows.push(["Security headers",o.headers_missing.length?`${o.headers_missing.length} missing: ${o.headers_missing.join(", ")}`:"all common headers present"]);
|
|
222
|
+
if(o.registrar)rows.push(["Registrar",o.registrar]);
|
|
223
|
+
if((o.open_ports||[]).length)rows.push(["Open ports",o.open_ports.join(", ")]);
|
|
224
|
+
if(o.email_security)rows.push(["Email auth",`SPF: ${o.email_security.spf_note||"?"} · DMARC: ${o.email_security.dmarc_policy||o.email_security.dmarc_note||"?"}`]);
|
|
225
|
+
if(o.subdomains)rows.push(["Subdomains",`${o.subdomains} from certificate transparency`]);
|
|
226
|
+
if(!rows.length)return "";
|
|
227
|
+
return `<div class="dc"><div class="h">External recon <span class="ct">${esc(o.host||"")}</span></div><div class="b"><table class="dtbl"><tbody>${rows.map(([k,v])=>`<tr data-goto="infra"><td>${esc(k)}</td><td style="max-width:60%">${esc(v)}</td></tr>`).join("")}</tbody></table>
|
|
228
|
+
<div class="dnote">Collected live by <span class="mono">proxy2model osint</span>, not from the capture.</div></div></div>`;
|
|
229
|
+
}
|
|
230
|
+
function vMore(){
|
|
231
|
+
const st=c.api_state||{},both=st.BOTH||0,rt=st.RUNTIME_ONLY||0,so=st.STATIC_ONLY||0,eptot=both+rt+so;
|
|
232
|
+
const cap=D.stats||{},roles=D.roles||[];
|
|
233
|
+
const reqTot=EVL.length;
|
|
234
|
+
const sev={hot:0,warn:0,info:0};PRIO.forEach(p=>sev[p.sev]++);
|
|
235
|
+
const topUnk=tally(D.unknowns||[],u=>u.type);
|
|
236
|
+
const sigTop=tally(PRIO.flatMap(p=>p.sig.map(x=>x.ttl)),t=>t).slice(0,4);
|
|
237
|
+
const tpReq=(D.third_parties||[]).reduce((a,t)=>a+(t.requests||(t.evidence||[]).length),0);
|
|
238
|
+
const kpi=(n,k,sub,col,go)=>`<div class="dk" style="--c:${col}" data-goto="${go}"><div class="n">${fmtN(n)}</div><div class="k">${esc(k)}</div><div class="s">${esc(sub)}</div></div>`;
|
|
239
|
+
|
|
240
|
+
/* methods + status mix from the evidence table */
|
|
241
|
+
const meth=tally(EVL,e=>e.method||"?"), stc=tally(EVL,e=>statusClass(e.status));
|
|
242
|
+
const stOrder=["2xx","3xx","4xx","5xx","?","1xx"].map(k=>({label:k==="?"?"no status":k,v:(stc.find(x=>x[0]===k)||[0,0])[1],color:SC_COL[k]}));
|
|
243
|
+
/* busiest endpoints */
|
|
244
|
+
const topEp=[...EP].filter(e=>e.requests>0).sort((a,b)=>b.requests-a.requests||a.label.localeCompare(b.label)).slice(0,8);
|
|
245
|
+
/* where requests went by host */
|
|
246
|
+
const hostN=tally(EVL,e=>e.host);
|
|
247
|
+
/* roles */
|
|
248
|
+
const roleN=tally(EVL,e=>e.role||null);
|
|
249
|
+
const roleCol=["var(--observed)","var(--inferred)","var(--ok)","var(--signal)","var(--warn)","var(--l2)"];
|
|
250
|
+
/* cookies */
|
|
251
|
+
const ck=D.cookies||[],ckBad=ck.filter(x=>!x.httponly||!x.secure).length;
|
|
252
|
+
const layers=[
|
|
253
|
+
["Edge","var(--l1)",c.hosts||0,"hosts"+((D.hosts||[]).some(h=>Object.keys(h.tech||{}).length)?" + tech":""),"graph"],
|
|
254
|
+
["Routes","var(--l2)",c.routes||0,"pages and states","surface"],
|
|
255
|
+
["Client code","var(--l3)",c.scripts||0,"scripts","code"],
|
|
256
|
+
["APIs","var(--l4)",(c.endpoints||0),`${c.parameters||0} parameters`,"surface"],
|
|
257
|
+
["Trust","var(--l5)",(c.third_parties||0)+(c.auth||0)+(c.cookies||0),`${c.third_parties||0} third · ${c.auth||0} auth · ${c.cookies||0} cookies`,"trust"],
|
|
258
|
+
["Unknowns","var(--l6)",c.unknowns||0,"named gaps","unknowns"]];
|
|
259
|
+
|
|
260
|
+
return ` <div class="dgrid dg3">
|
|
261
|
+
<div class="dc"><div class="h">Code vs runtime <span class="ct">${eptot} endpoints</span></div><div class="b">
|
|
262
|
+
<div class="dwrap">${donut([{v:both,color:"var(--ok)",label:"seen in code and traffic"},{v:rt,color:"var(--observed)",label:"traffic only"},{v:so,color:"var(--signal)",label:"code only"}],150,20,String(eptot),"ENDPOINTS")}
|
|
263
|
+
<div class="dleg">
|
|
264
|
+
<div class="r" data-goto="endpoints" data-seen="both"><span class="sw" style="background:var(--ok)"></span><span class="l">Code + traffic</span><b>${both}</b><span class="pc">${pct(both,eptot)}</span></div>
|
|
265
|
+
<div class="r" data-goto="endpoints" data-seen="traffic"><span class="sw" style="background:var(--observed)"></span><span class="l">Traffic only</span><b>${rt}</b><span class="pc">${pct(rt,eptot)}</span></div>
|
|
266
|
+
<div class="r" data-goto="endpoints" data-seen="code"><span class="sw" style="background:var(--signal)"></span><span class="l">Code only</span><b>${so}</b><span class="pc">${pct(so,eptot)}</span></div></div></div>
|
|
267
|
+
<div class="dnote">Code only = named in the client code but never called in this capture.</div></div></div>
|
|
268
|
+
|
|
269
|
+
<div class="dc"><div class="h">Traffic shape <span class="ct">${fmtN(reqTot)} requests</span></div><div class="b">
|
|
270
|
+
${stacked(stOrder)}
|
|
271
|
+
<div style="height:16px"></div>
|
|
272
|
+
${hbars(meth.slice(0,6).map(([k,v])=>({label:k,v,color:k==="GET"?"var(--muted)":"var(--signal)"})),{sm:true})}</div></div>
|
|
273
|
+
|
|
274
|
+
<div class="dc"><div class="h">Worth a look <span class="ct">${PRIO.length} item${PRIO.length===1?"":"s"}</span></div><div class="b">
|
|
275
|
+
<div class="att">
|
|
276
|
+
<div class="a" style="--c:var(--signal)" data-goto="priorities"><div class="n">${sev.hot}</div><div class="k">High</div></div>
|
|
277
|
+
<div class="a" style="--c:var(--warn)" data-goto="priorities"><div class="n">${sev.warn}</div><div class="k">Medium</div></div>
|
|
278
|
+
<div class="a" style="--c:var(--observed)" data-goto="priorities"><div class="n">${sev.info}</div><div class="k">Note</div></div></div>
|
|
279
|
+
<div style="height:14px"></div>${hbars(sigTop.map(([k,v])=>({label:k,v,color:"var(--signal)",attr:'data-goto="priorities"'})),{wide:true})}
|
|
280
|
+
<div class="dnote">Ranked leads to verify, not findings. Each says what to check next.</div>
|
|
281
|
+
${ckBad?`<div class="dnote" style="margin-top:6px"><span class="wrnt">●</span> ${ckBad} cookie${ckBad===1?"":"s"} missing HttpOnly or Secure</div>`:""}</div></div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="dc" style="margin-bottom:14px"><div class="h">Capture timeline <span class="ct">each bar is a request, in capture order</span></div><div class="b">
|
|
285
|
+
${captureStrip()}</div></div>
|
|
286
|
+
|
|
287
|
+
<div class="dgrid dg2">
|
|
288
|
+
<div class="dc"><div class="h">Start here <button class="lnk" data-goto="priorities">All ${PRIO.length} →</button></div><div class="b dlist">
|
|
289
|
+
${PRIO.slice(0,6).map((p,i)=>{const e=p.ep,l=p.sig[0];return `<div class="it" data-ep="${esc(e.id)}" style="--c:${p.sev==="hot"?"var(--signal)":p.sev==="warn"?"var(--warn)":"var(--observed)"}"><span class="rk">${i+1}</span><div class="tx"><div class="tt">${methodm(e.method)}<span class="p">${esc(e.path)}</span></div><div class="ds">${esc(l.ttl)}${p.sig.length>1?` · +${p.sig.length-1} more`:""}</div></div></div>`;}).join("")||'<div class="empty">Nothing stands out.</div>'}</div></div>
|
|
290
|
+
<div class="dc"><div class="h">Busiest endpoints <button class="lnk" data-goto="endpoints">All →</button></div><div class="b">
|
|
291
|
+
${topEp.length?hbars(topEp.map(e=>({label:(e.method!=="*"?e.method+" ":"")+e.path,v:e.requests,attr:`data-ep="${esc(e.id)}"`,color:e.state==="STATIC_ONLY"?"var(--signal)":"var(--observed)"}))):'<div class="empty">No endpoint was requested.</div>'}</div></div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="dgrid dg21">
|
|
295
|
+
<div class="dc"><div class="h">${roles.length>1?"Roles":"Credentials"} <span class="ct">${roles.length>1?roles.length+" roles":""}</span></div><div class="b">
|
|
296
|
+
${roles.length>1?hbars(roleN.map(([k,v],i)=>({label:k,v,color:roleCol[i%roleCol.length],attr:'data-goto="crossrole"'}))):
|
|
297
|
+
((D.auth||[]).length?`<table class="dtbl"><tbody>${D.auth.map(a=>`<tr data-goto="trust"><td>${esc(a.label)}</td><td>${a.endpoints.length} endpoint${a.endpoints.length===1?"":"s"}</td></tr>`).join("")}</tbody></table>`:'<div class="empty">No credentials observed.</div>')}
|
|
298
|
+
${roles.length>1?`<div class="dnote">Requests per role. <a href="#crossrole" data-goto="crossrole">Compare what each role reached →</a></div>`:""}</div></div>
|
|
299
|
+
<div class="dc"><div class="h">Third parties <button class="lnk" data-goto="supply">All →</button></div><div class="b">
|
|
300
|
+
${(D.third_parties||[]).length?hbars([...D.third_parties].sort((a,b)=>(b.requests||(b.evidence||[]).length)-(a.requests||(a.evidence||[]).length)).slice(0,7).map(t=>({label:t.label,v:t.requests||(t.evidence||[]).length,color:"var(--l5)",attr:'data-goto="supply"'}))):'<div class="empty">No third-party hosts.</div>'}</div></div>
|
|
301
|
+
<div class="dc"><div class="h">Open questions <button class="lnk" data-goto="unknowns">All →</button></div><div class="b">
|
|
302
|
+
${topUnk.length?hbars(topUnk.slice(0,6).map(([k,v])=>({label:openTitle(k),v,color:"var(--l6)",attr:'data-goto="unknowns"'})),{wide:true}):'<div class="empty">Nothing left open.</div>'}</div></div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div class="lstrip">${layers.map(([n,col,v,d,go])=>`<div class="ls" style="--c:${col}" data-goto="${go}"><div class="ln">${esc(n)}</div><div class="n">${fmtN(v)}</div><div class="d">${esc(d)}</div></div>`).join("")}</div>
|
|
306
|
+
|
|
307
|
+
<div class="dgrid dg2">
|
|
308
|
+
<div class="dc"><div class="h">Hosts <span class="ct">${(D.hosts||[]).length} first-party</span></div><div class="b">
|
|
309
|
+
<table class="dtbl"><tbody>${(D.hosts||[]).map(h=>`<tr data-goto="graph"><td class="mono">${esc(h.label)}<span style="color:var(--faint)"> ${esc((h.schemes||[]).join("/"))}${(h.ports||[]).length?":"+esc(h.ports.join(",")):""}</span></td><td>${esc(Object.entries(h.tech||{}).map(([k,v])=>v?`${k}: ${v}`:k).join(" · ")||"—")}</td></tr>`).join("")||'<tr><td>—</td></tr>'}</tbody></table>
|
|
310
|
+
${hostN.length>1?`<div style="height:12px"></div>${hbars(hostN.slice(0,5).map(([k,v])=>({label:k,v,color:"var(--l1)",attr:'data-goto="inventory"'})),{sm:true})}`:""}</div></div>
|
|
311
|
+
<div class="dc"><div class="h">Capture coverage</div><div class="b"><table class="dtbl"><tbody>
|
|
312
|
+
<tr><td>Requests parsed</td><td>${cap.parsed??cap.exchanges??reqTot} of ${cap.items??"—"}${cap.skipped?` · <span class="wrnt">${cap.skipped} skipped</span>`:""}</td></tr>
|
|
313
|
+
<tr><td>Static assets (not mapped)</td><td>${cap.static_assets??0}</td></tr>
|
|
314
|
+
<tr><td>CORS preflights</td><td>${cap.preflight??0}</td></tr>
|
|
315
|
+
<tr><td>Roles</td><td>${esc(roles.join(", ")||"none")}</td></tr></tbody></table>
|
|
316
|
+
<div class="dnote">An absence here is an absence in the <b>capture</b>, never a statement about the target.</div></div></div>
|
|
317
|
+
</div>
|
|
318
|
+
${D.osint?`<div class="dgrid" style="grid-template-columns:minmax(0,1fr)">${osintCard()}</div>`:""}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/* ---------- at a glance: screenshot, infrastructure, stack ---------- */
|
|
323
|
+
const STACK=D.stack||[];
|
|
324
|
+
const STACK_COL={"Edge & CDN":"var(--l6)","Web server":"var(--l1)","Language & framework":"var(--l4)","Frontend":"var(--l3)","CMS & platform":"var(--l2)","API":"var(--inferred)","Services":"var(--l5)"};
|
|
325
|
+
const STACK_SHORT={"Edge & CDN":"Edge","Web server":"Server","Language & framework":"App","Frontend":"Frontend","CMS & platform":"Platform","API":"API"};
|
|
326
|
+
const HDR_LABEL={"strict-transport-security":"HSTS","content-security-policy":"CSP","x-frame-options":"X-Frame-Options","x-content-type-options":"nosniff","referrer-policy":"Referrer-Policy","permissions-policy":"Permissions-Policy","cross-origin-opener-policy":"COOP","cross-origin-resource-policy":"CORP"};
|
|
327
|
+
/* the host the story is about: the one recon looked at, else the busiest */
|
|
328
|
+
function primaryFirst(){
|
|
329
|
+
const rec=(D.osint||{}).host;
|
|
330
|
+
return [...(D.hosts||[])].sort((a,b)=>(b.label===rec)-(a.label===rec)||(b.evidence||[]).length-(a.evidence||[]).length);
|
|
331
|
+
}
|
|
332
|
+
function techPill(t){
|
|
333
|
+
const ev=(t.evidence||[])[0],how=t.how?`seen in ${t.how}`:"";
|
|
334
|
+
return `<button class="tp" ${ev?`data-ev="${ev}"`:""} title="${esc(how+(ev?" · "+EVD(ev):""))}"><i style="background:${STACK_COL[t.category]||"var(--l5)"}"></i>${esc(t.name)}${t.version?`<small>${esc(t.version)}</small>`:""}</button>`;
|
|
335
|
+
}
|
|
336
|
+
function stackCard(){
|
|
337
|
+
const order=["Edge & CDN","Web server","Language & framework","Frontend","CMS & platform","API"];
|
|
338
|
+
const layers=order.map(c=>[c,STACK.filter(t=>t.category===c)]).filter(x=>x[1].length);
|
|
339
|
+
const svc=STACK.filter(t=>t.category==="Services");
|
|
340
|
+
if(!layers.length&&!svc.length)return "";
|
|
341
|
+
const kinds=new Map();svc.forEach(t=>{const k=t.kind||"Other";(kinds.get(k)||kinds.set(k,[]).get(k)).push(t);});
|
|
342
|
+
return `<div class="dc"><div class="h">Technology stack <span class="ct">${STACK.length} detected</span></div><div class="b">
|
|
343
|
+
${layers.length?`<div class="smap"><div class="sn you">Browser</div>${layers.map(([c,ts])=>`<div class="sa">→</div><div class="sn"><span class="sl">${esc(STACK_SHORT[c]||c)}</span>${ts.map(techPill).join("")}</div>`).join("")}</div>`:""}
|
|
344
|
+
${svc.length?`<div class="svc">${[...kinds.entries()].sort((a,b)=>a[0].localeCompare(b[0])).map(([k,ts])=>`<span class="sk">${esc(k)}</span><span class="sv">${ts.map(techPill).join("")}</span>`).join("")}</div>`:""}
|
|
345
|
+
<div class="dnote">Read from headers, cookies, pages and the hosts the app talks to. Hints, not a version scan.</div></div></div>`;
|
|
346
|
+
}
|
|
347
|
+
function infraRows(){
|
|
348
|
+
const rows=[],o=D.osint||{},hs=primaryFirst();
|
|
349
|
+
if(hs.length){const h=hs[0],sc=(h.schemes||[]).join("/"),pt=(h.ports||[]).join(", ");
|
|
350
|
+
rows.push(["Host",`${esc(h.label)}${hs.length>1?`<small>+${hs.length-1} more</small>`:""}<small>${esc([sc,pt].filter(Boolean).join(" "))}</small>`]);}
|
|
351
|
+
if(o.hosting&&o.hosting.network)rows.push(["Hosting",esc(o.hosting.network+(o.hosting.country?" · "+o.hosting.country:""))]);
|
|
352
|
+
if((o.ips||[]).length)rows.push(["IP",esc(o.ips.join(", "))]);
|
|
353
|
+
if(o.tls&&o.tls.issuer)rows.push(["TLS",esc(o.tls.issuer)+(o.tls.days_until_expiry!=null?`<small>${o.tls.days_until_expiry} days left</small>`:"")]);
|
|
354
|
+
if(o.registrar)rows.push(["Registrar",esc(o.registrar)]);
|
|
355
|
+
const dn=[];if((o.nameservers||[]).length)dn.push(o.nameservers.length+" nameservers");if(o.subdomains)dn.push(o.subdomains+" subdomains");
|
|
356
|
+
if(dn.length)rows.push(["DNS",esc(dn.join(" · "))]);
|
|
357
|
+
const em=o.email_security||{},mail=[];if(em.spf)mail.push("SPF");if(em.dmarc_policy)mail.push("DMARC p="+em.dmarc_policy);
|
|
358
|
+
if(mail.length)rows.push(["Mail auth",esc(mail.join(" · "))]);
|
|
359
|
+
const hp=(o.headers_present||[]).map(h=>HDR_LABEL[h]||h);
|
|
360
|
+
if(hp.length)rows.push(["Headers set",esc(hp.slice(0,4).join(", "))+(hp.length>4?`<small>+${hp.length-4}</small>`:"")]);
|
|
361
|
+
return rows.slice(0,8);
|
|
362
|
+
}
|
|
363
|
+
function infraCard(){
|
|
364
|
+
const rows=infraRows();if(rows.length<2)return "";
|
|
365
|
+
return `<div class="dc"><div class="h">Infrastructure</div><div class="b"><div class="irows">${rows.map(([k,v])=>`<div class="k">${esc(k)}</div><div class="v">${v}</div>`).join("")}</div>${D.osint?`<div class="dnote">Hosting, TLS and DNS come from external recon.</div>`:""}</div></div>`;
|
|
366
|
+
}
|
|
367
|
+
function shotCard(){
|
|
368
|
+
if(!D.screenshot)return "";
|
|
369
|
+
const hs=primaryFirst(),h=hs[0];
|
|
370
|
+
return `<div class="shotcard"><div class="sbar"><i></i><i></i><i></i><span>${esc(h?`${(h.schemes||["https"])[0]}://${h.label}/`:D.app)}</span></div><button class="simg" data-shot aria-label="Enlarge the screenshot"><img src="${esc(D.screenshot)}" alt="Screenshot of ${esc(h?h.label:D.app)}"></button></div>`;
|
|
371
|
+
}
|
|
372
|
+
function changesRibbon(){
|
|
373
|
+
if(!hasChanges())return "";
|
|
374
|
+
const c=D.changes,s=c.summary,t=c.to||{},f=c.from||{};
|
|
375
|
+
return `<button class="chgbar" data-goto="changes"><b>Since build ${esc(f.id)}${f.label?` (${esc(f.label)})`:""}</b><span>${s.new_workflows} new workflow${s.new_workflows===1?"":"s"}</span><span>${s.changed} changed</span><span>${s.new} new</span><span>${s.gone} gone</span><i>See what moved →</i></button>`;
|
|
376
|
+
}
|
|
377
|
+
function glance(){
|
|
378
|
+
const shot=shotCard(),a=infraCard(),b=stackCard();
|
|
379
|
+
if(!shot&&!a&&!b)return "";
|
|
380
|
+
return `<div class="band${shot?" shot":""}">${shot}${a}${b}</div>`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* several leads with the same reason: the reason once, then each endpoint as a link */
|
|
384
|
+
function leadGroupCard(g,i){
|
|
385
|
+
const col=g.sev==="hot"?"var(--signal)":g.sev==="warn"?"var(--warn)":"var(--observed)",max=6;
|
|
386
|
+
return `<div class="lead" style="--c:${col}"><span class="num">${i+1}</span><div>
|
|
387
|
+
<div class="w" style="margin-top:0">${esc(g.lead.ttl)} <span style="color:var(--faint);font:600 12px var(--mono)">${g.items.length} endpoints</span></div>
|
|
388
|
+
<div class="y">${esc(g.lead.why)}</div><div class="nx"><b>Do next</b>${esc(g.lead.next)}</div>
|
|
389
|
+
<div class="eps">${g.items.slice(0,max).map(([p])=>`<button data-open-ep="${esc(p.ep.id)}">${methodm(p.ep.method)}<span>${esc(p.ep.path)}</span></button>`).join("")}${g.items.length>max?`<span class="more">+ ${g.items.length-max} more on the To check page</span>`:""}</div></div>
|
|
390
|
+
<div class="acts"><button class="abtn pri" data-goto="priorities">See all</button></div></div>`;
|
|
391
|
+
}
|
|
392
|
+
function leadCard(p,i){
|
|
393
|
+
const e=p.ep,l=p.sig[0],col=p.sev==="hot"?"var(--signal)":p.sev==="warn"?"var(--warn)":"var(--observed)";
|
|
394
|
+
return `<div class="lead" style="--c:${col}"><span class="num">${i+1}</span><div>
|
|
395
|
+
<div class="t">${methodm(e.method)}<span>${esc(e.path)}</span></div>
|
|
396
|
+
<div class="w">${esc(l.ttl)}${p.sig.length>1?` <span style="color:var(--faint);font-weight:500">+${p.sig.length-1} more</span>`:""}</div>
|
|
397
|
+
<div class="y">${esc(l.why)}</div><div class="nx"><b>Do next</b>${esc(l.next)}</div></div>
|
|
398
|
+
<div class="acts"><button class="abtn pri" data-open-ep="${esc(e.id)}">Open</button><button class="abtn" data-lead-map="${esc(e.id)}">On the map</button></div></div>`;
|
|
399
|
+
}
|
|
400
|
+
function vOverview(){
|
|
401
|
+
const st=c.api_state||{},both=st.BOTH||0,rt=st.RUNTIME_ONLY||0,so=st.STATIC_ONLY||0,eptot=both+rt+so;
|
|
402
|
+
const hot=PRIO.filter(p=>p.sev==="hot").length,roles=D.roles||[],reqTot=EVL.length;
|
|
403
|
+
const sum=[`<b>${c.endpoints||0}</b> API endpoints on <b>${c.routes||0}</b> pages, from <b>${fmtN(reqTot)}</b> captured requests${roles.length>1?` as <b>${roles.length}</b> roles`:""}.`];
|
|
404
|
+
if(so)sum.push(`<b>${so}</b> ${so===1?"endpoint is":"endpoints are"} named in the code but never called.`);
|
|
405
|
+
sum.push(PRIO.length?"Start with what is below.":"Nothing stands out. The map is the next stop.");
|
|
406
|
+
const stc=tally(EVL,e=>statusClass(e.status));
|
|
407
|
+
const stOrder=["2xx","3xx","4xx","5xx"].map(k=>({label:k,v:(stc.find(x=>x[0]===k)||[0,0])[1],color:SC_COL[k]}));
|
|
408
|
+
const unk=c.unknowns||0;
|
|
409
|
+
return `<div class="ovh"><div><h1>${esc(D.app)}</h1><p class="sum">${sum.join(" ")}</p>
|
|
410
|
+
<div class="meta">scope ${esc((D.scope||[]).join(", ")||"none")} · ${esc(D.generated||"")}</div></div>
|
|
411
|
+
<div class="big4">
|
|
412
|
+
<button data-goto="endpoints"><div class="n">${c.endpoints||0}</div><div class="k">API endpoints</div></button>
|
|
413
|
+
<button data-goto="inventory"><div class="n">${fmtN(reqTot)}</div><div class="k">Requests</div></button>
|
|
414
|
+
<button class="${hot||weakCookies.length?"hot":""}" data-goto="priorities"><div class="n">${PRIO.length+weakCookies.length}</div><div class="k">To check</div></button>
|
|
415
|
+
<button data-goto="unknowns"><div class="n">${unk}</div><div class="k">Open questions</div></button></div></div>
|
|
416
|
+
|
|
417
|
+
${changesRibbon()}
|
|
418
|
+
${glance()}
|
|
419
|
+
|
|
420
|
+
<div class="ovg"><div>
|
|
421
|
+
<div class="sech"><h2>Check these first</h2><span class="s">ranked leads to verify, not findings</span>${PRIO.length+weakCookies.length>5?`<button class="lnk" data-goto="priorities">All ${PRIO.length+weakCookies.length} →</button>`:""}</div>
|
|
422
|
+
${leadGroups(PRIO.map((p,i)=>[p,i]),p=>p.sig[0],p=>p.sev).slice(0,4).map((g,i)=>g.items.length<2?leadCard(g.items[0][0],i):leadGroupCard(g,i)).join("")||'<div class="dc"><div class="b"><div class="empty">Nothing stands out in this capture.</div></div></div>'}
|
|
423
|
+
</div><div class="side">
|
|
424
|
+
<div class="dc"><div class="h">What the code names vs what ran</div><div class="b"><div class="dwrap">${donut([{v:both,color:"var(--ok)",label:"in code and traffic"},{v:rt,color:"var(--observed)",label:"traffic only"},{v:so,color:"var(--signal)",label:"code only"}],130,18,String(eptot),"ENDPOINTS")}
|
|
425
|
+
<div class="dleg"><div class="r" data-goto="endpoints" data-seen="both"><span class="sw" style="background:var(--ok)"></span><span class="l">Both</span><b>${both}</b></div>
|
|
426
|
+
<div class="r" data-goto="endpoints" data-seen="traffic"><span class="sw" style="background:var(--observed)"></span><span class="l">Traffic only</span><b>${rt}</b></div>
|
|
427
|
+
<div class="r" data-goto="endpoints" data-seen="code"><span class="sw" style="background:var(--signal)"></span><span class="l">Code only</span><b>${so}</b></div></div></div></div></div>
|
|
428
|
+
<div class="dc"><div class="h">Responses</div><div class="b">${stacked(stOrder)}</div></div>
|
|
429
|
+
${roles.length>1?`<div class="dc"><div class="h">Roles in this capture</div><div class="b"><div class="dtags">${roles.map(r=>`<span class="dtag"><b>${esc(r)}</b></span>`).join("")}</div><div class="dnote"><a href="#crossrole" data-goto="crossrole">See what each role reached →</a></div></div></div>`:""}
|
|
430
|
+
</div></div>
|
|
431
|
+
|
|
432
|
+
<div class="sech"><h2>Explore</h2></div>
|
|
433
|
+
<div class="tiles">
|
|
434
|
+
<button class="tile" data-goto="graph"><b>Map</b><span>How pages, scripts and endpoints connect.</span></button>
|
|
435
|
+
<button class="tile" data-goto="inventory"><b>Requests</b><span>Every request, like Burp's HTTP history.</span></button>
|
|
436
|
+
<button class="tile" data-goto="ask"><b>Ask</b><span>A plain question, or a BQL query.</span></button>
|
|
437
|
+
<button class="tile" data-goto="unknowns"><b>Open questions</b><span>What this capture could not show.</span></button></div>
|
|
438
|
+
|
|
439
|
+
<details class="more"><summary>More numbers <span>timeline, busiest endpoints, third parties, hosts</span></summary><div class="inner">${vMore()}</div></details>`;
|
|
440
|
+
}
|
|
441
|
+
function initOverview(){
|
|
442
|
+
$$("[data-open-ep]").forEach(b=>b.onclick=e=>{e.stopPropagation();EPV.sel=b.dataset.openEp;EPV.q="";EPV.seen="all";view="endpoints";render();});
|
|
443
|
+
$$("[data-seen]").forEach(b=>b.onclick=e=>{e.stopPropagation();EPV.seen=b.dataset.seen;EPV.q="";EPV.sel=null;view="endpoints";render();});
|
|
444
|
+
$$("[data-ev]",$("#main")).forEach(b=>b.onclick=e=>{e.stopPropagation();openEv(b.dataset.ev);});
|
|
445
|
+
const sh=$("[data-shot]");
|
|
446
|
+
if(sh)sh.onclick=()=>{const d=document.createElement("div");d.className="lightbox";d.innerHTML=`<img src="${esc(D.screenshot)}" alt="Screenshot">`;d.onclick=()=>d.remove();document.body.append(d);};
|
|
447
|
+
addEventListener("keydown",function k(e){if(e.key==="Escape"){const l=$(".lightbox");if(l)l.remove();removeEventListener("keydown",k);}});
|
|
448
|
+
$$("[data-lead-map]").forEach(b=>b.onclick=e=>{e.stopPropagation();MAPS.sel=b.dataset.leadMap;MAPS.path=null;if(MAPS.mode==="focus")MAPS.focusId=MAPS.sel;view="graph";render();});
|
|
449
|
+
}
|
|
450
|
+
/* one bar per request in capture order, coloured by status class; long captures are bucketed */
|
|
451
|
+
function captureStrip(){
|
|
452
|
+
const n=EVL.length;if(!n)return '<div class="empty">No requests.</div>';
|
|
453
|
+
const W=1000,H=56,B=Math.min(n,W),per=n/B;
|
|
454
|
+
let bars="",lastRole=null,runs=[];
|
|
455
|
+
EVL.forEach(e=>{const r=e.role||"—";if(!runs.length||runs[runs.length-1].r!==r)runs.push({r,n:0});runs[runs.length-1].n++;});
|
|
456
|
+
const worst=["5xx","4xx","3xx","2xx","?","1xx"];
|
|
457
|
+
for(let i=0;i<B;i++){
|
|
458
|
+
const a=Math.floor(i*per),b=Math.max(a+1,Math.floor((i+1)*per));let cls="2xx",rank=9;
|
|
459
|
+
for(let k=a;k<b&&k<n;k++){const s=statusClass(EVL[k].status),rr=worst.indexOf(s);if(rr<rank){rank=rr;cls=s;}}
|
|
460
|
+
const h=cls==="5xx"?H:cls==="4xx"?H*.82:cls==="3xx"?H*.62:H*.5;
|
|
461
|
+
bars+=`<rect x="${(i*W/B).toFixed(2)}" y="${(H-h).toFixed(1)}" width="${B<120?(W/B*.8).toFixed(2):(W/B+.35).toFixed(2)}" height="${h.toFixed(1)}" fill="${SC_COL[cls]}" opacity=".85"/>`;
|
|
462
|
+
}
|
|
463
|
+
const rcol=["var(--observed)","var(--inferred)","var(--ok)","var(--signal)","var(--warn)","var(--l2)"];
|
|
464
|
+
const names=[...new Set(runs.map(r=>r.r))];
|
|
465
|
+
const rb=runs.length>1||runs[0].r!=="—"?`<div class="rolebar">${runs.map(r=>`<span style="flex:${r.n};background:${rcol[names.indexOf(r.r)%rcol.length]}" title="${esc(r.r)}: ${r.n} requests">${esc(r.r)}</span>`).join("")}</div>`:"";
|
|
466
|
+
return `<svg class="cstrip" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="Requests in capture order, coloured by status">${bars}</svg>${rb}
|
|
467
|
+
<div class="stkleg" style="margin-top:8px"><span class="k"><span class="sw" style="background:var(--ok)"></span>2xx</span><span class="k"><span class="sw" style="background:var(--observed)"></span>3xx</span><span class="k"><span class="sw" style="background:var(--warn)"></span>4xx</span><span class="k"><span class="sw" style="background:var(--bad)"></span>5xx</span></div>`;
|
|
468
|
+
}
|
|
469
|
+
"""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Fonts and type for the offline report.
|
|
2
|
+
|
|
3
|
+
The typefaces are embedded as base64 so the single-file report looks the same with no network. The
|
|
4
|
+
CSP allows `font-src data:` and nothing else. Nippo carries the fa1c0n field-notes look; it is a
|
|
5
|
+
subset (printable ASCII and a few marks), so Inter stands in for any other glyph.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import functools
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "fonts")
|
|
15
|
+
_FACES = [("Nippo", f"nippo-{w}.woff2", str(w)) for w in (200, 300, 400, 500, 700)] + [
|
|
16
|
+
("Inter", "inter-latin-wght-normal.woff2", "100 900"),
|
|
17
|
+
("JetBrains Mono", "jetbrains-mono-latin-wght-normal.woff2", "100 900")]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@functools.lru_cache(maxsize=1)
|
|
21
|
+
def font_css() -> str:
|
|
22
|
+
"""@font-face rules with the files inlined; empty if the files are missing (system fonts then)."""
|
|
23
|
+
out = []
|
|
24
|
+
for family, fn, weight in _FACES:
|
|
25
|
+
try:
|
|
26
|
+
with open(os.path.join(_DIR, fn), "rb") as f:
|
|
27
|
+
b64 = base64.b64encode(f.read()).decode("ascii")
|
|
28
|
+
except OSError:
|
|
29
|
+
return ""
|
|
30
|
+
out.append(f'@font-face{{font-family:"{family}";font-style:normal;font-weight:{weight};font-display:swap;'
|
|
31
|
+
f'src:url(data:font/woff2;base64,{b64}) format("woff2")}}')
|
|
32
|
+
return "\n".join(out)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# the field-notes type: Nippo throughout, light for big numbers, medium for titles; code stays mono
|
|
36
|
+
TYPE_CSS = r"""
|
|
37
|
+
:root{--sans:"Nippo","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
38
|
+
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
39
|
+
--display:"Nippo","Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
40
|
+
body{letter-spacing:.005em}
|
|
41
|
+
h1.vt,.ovh h1,.inv-bar h1,.mtop h1{font-family:var(--display);font-weight:500;letter-spacing:-.02em}
|
|
42
|
+
h1.vt{font-size:28px}
|
|
43
|
+
.ovh h1{font-weight:300;letter-spacing:-.03em}
|
|
44
|
+
.big4 .n,.dk .n,.ls .n,.kpi .n,.layer .nn{font-family:var(--display);font-weight:300;font-variant-numeric:tabular-nums;letter-spacing:-.03em}
|
|
45
|
+
nav.views h3{color:var(--signal);letter-spacing:.2em}
|
|
46
|
+
.sech h2,.dc>.h,.tile b,.card>.hd,.nlabel,.ahd,.lead .w{font-family:var(--display);font-weight:500;letter-spacing:-.01em}
|
|
47
|
+
.sech h2{font-size:17px}
|
|
48
|
+
.dc>.h,.card>.hd{font-size:14px}
|
|
49
|
+
.vsub,.ovh .sum{font-size:15.5px;line-height:1.6}
|
|
50
|
+
.kicker,nav.views h3,.minsp h3,.dnote{font-family:var(--mono)}
|
|
51
|
+
"""
|