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
proxy2model/db/bql_js.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""The BQL engine for the offline report — a JavaScript port of bql.py.
|
|
2
|
+
|
|
3
|
+
The report is a single file with no server, so its query console cannot call
|
|
4
|
+
SQLite. This runs the same language over the data embedded in the page. It is
|
|
5
|
+
kept in lockstep with `bql.py` by tests/test_bql_parity.py, which runs one query
|
|
6
|
+
corpus through both engines and compares the answers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
BQL_JS = r"""
|
|
10
|
+
const BQL=(function(){
|
|
11
|
+
const OPS=["eq","ne","cont","ncont","like","regex","gt","gte","lt","lte"];
|
|
12
|
+
const DEFAULT_LIMIT=100,MAX_LIMIT=5000;
|
|
13
|
+
class BQLError extends Error{}
|
|
14
|
+
const fail=m=>{throw new BQLError(m);};
|
|
15
|
+
const cmp=(a,b)=>a<b?-1:a>b?1:0;
|
|
16
|
+
|
|
17
|
+
/* ---- lexing ---- */
|
|
18
|
+
const TOKEN=/\s*(?:(\()|(\))|([A-Za-z_][\w.\-]*:(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s()]+))|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|([^\s()"']+))/y;
|
|
19
|
+
function unquote(s){return s.length>=2&&s[0]===s[s.length-1]&&(s[0]==='"'||s[0]==="'")?s.slice(1,-1).split("\\"+s[0]).join(s[0]):s;}
|
|
20
|
+
function lex(text){
|
|
21
|
+
const toks=[];let pos=0;text=text.trim();
|
|
22
|
+
while(pos<text.length){
|
|
23
|
+
TOKEN.lastIndex=pos;const m=TOKEN.exec(text);
|
|
24
|
+
if(!m||TOKEN.lastIndex===pos)fail("cannot read the query near "+JSON.stringify(text.slice(pos,pos+20)));
|
|
25
|
+
pos=TOKEN.lastIndex;
|
|
26
|
+
if(m[1])toks.push(["lp","("]);
|
|
27
|
+
else if(m[2])toks.push(["rp",")"]);
|
|
28
|
+
else if(m[3]){const i=m[3].indexOf(":");toks.push(["term",[m[3].slice(0,i),unquote(m[3].slice(i+1))]]);}
|
|
29
|
+
else if(m[4])toks.push(["text",unquote(m[4])]);
|
|
30
|
+
else if(["AND","OR","NOT"].includes(m[5].toUpperCase()))toks.push([m[5].toUpperCase(),m[5].toUpperCase()]);
|
|
31
|
+
else toks.push(["word",m[5]]);
|
|
32
|
+
}
|
|
33
|
+
return toks;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ---- parsing ---- */
|
|
37
|
+
function parse(toks){
|
|
38
|
+
let i=0;
|
|
39
|
+
const peek=()=>i<toks.length?toks[i][0]:null;
|
|
40
|
+
const take=()=>toks[i++];
|
|
41
|
+
function or_(){let n=and_();while(peek()==="OR"){take();n=["or",n,and_()];}return n;}
|
|
42
|
+
function and_(){let n=not_();while(["AND","term","text","lp","NOT"].includes(peek())){if(peek()==="AND")take();n=["and",n,not_()];}return n;}
|
|
43
|
+
function not_(){if(peek()==="NOT"){take();return["not",not_()];}return atom();}
|
|
44
|
+
function atom(){
|
|
45
|
+
const k=peek();
|
|
46
|
+
if(k==="lp"){take();const n=or_();if(peek()!=="rp")fail("missing )");take();return n;}
|
|
47
|
+
if(k==="term"){const [,[head,raw]]=take();const parts=head.split(".");let op="eq";
|
|
48
|
+
if(parts.length>1&&OPS.includes(parts[parts.length-1]))op=parts.pop();
|
|
49
|
+
return["term",parts.join("."),op,raw];}
|
|
50
|
+
if(k==="text")return["text",take()[1]];
|
|
51
|
+
fail(k===null?"expected a term like field:value":"unexpected "+JSON.stringify(toks[i][1]));
|
|
52
|
+
}
|
|
53
|
+
const n=or_();
|
|
54
|
+
if(i<toks.length)fail("unexpected "+JSON.stringify(toks[i][1]));
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ---- fields ---- */
|
|
59
|
+
const REQ={
|
|
60
|
+
"req.method":[r=>r.method,"text"],"req.host":[r=>r.host,"text"],"req.path":[r=>r.path,"text"],
|
|
61
|
+
"req.template":[r=>r.path_template,"text"],"req.header":[r=>r.req_headers,"text"],
|
|
62
|
+
"req.body":[r=>r.req_body,"text"],"resp.code":[r=>r.status,"int"],"resp.mime":[r=>r.mime,"text"],
|
|
63
|
+
"resp.header":[r=>r.resp_headers,"text"],"resp.body":[r=>r.resp_body,"text"],
|
|
64
|
+
"role":[r=>r.role,"text"],"id":[r=>r.id,"int"],"source":[r=>r.source,"text"]};
|
|
65
|
+
const REQ_TEXT=["path","req_body","resp_body","req_headers","resp_headers","host"];
|
|
66
|
+
const asText=v=>v==null?null:typeof v==="boolean"?(v?"1":"0"):typeof v==="object"?JSON.stringify(v):String(v);
|
|
67
|
+
const NODE={
|
|
68
|
+
"node.id":[n=>n.id,"text"],"node.type":[n=>n.type,"text"],"node.label":[n=>n.label,"text"],
|
|
69
|
+
"node.layer":[n=>n.layer,"int"],"node.source":[n=>n.source,"text"],
|
|
70
|
+
"node.state":[n=>asText(n.attrs.api_state),"text"]};
|
|
71
|
+
const NODE_TEXT=["label","id"];
|
|
72
|
+
function EDGE(ctx){
|
|
73
|
+
const nd=id=>ctx.nodeById.get(id);
|
|
74
|
+
return{
|
|
75
|
+
"edge.type":[e=>e.type,"text"],"edge.state":[e=>e.state,"text"],"edge.src":[e=>e.src,"text"],"edge.dst":[e=>e.dst,"text"],
|
|
76
|
+
"edge.src.type":[e=>(nd(e.src)||{}).type,"text"],"edge.dst.type":[e=>(nd(e.dst)||{}).type,"text"],
|
|
77
|
+
"edge.src.label":[e=>(nd(e.src)||{}).label,"text"],"edge.dst.label":[e=>(nd(e.dst)||{}).label,"text"]};}
|
|
78
|
+
const EDGE_TEXT=["src","dst","type"];
|
|
79
|
+
const ATTR=/^[A-Za-z_][\w\-]*$/;
|
|
80
|
+
|
|
81
|
+
function targetOf(f,ctx){
|
|
82
|
+
if(f in REQ)return"requests";
|
|
83
|
+
if(f in EDGE(ctx))return"edges";
|
|
84
|
+
if(f in NODE||f==="node.evidence"||f==="node.role"||f.startsWith("node.attr."))return"nodes";
|
|
85
|
+
fail("unknown field "+JSON.stringify(f)+"; run `help` for the field list");
|
|
86
|
+
}
|
|
87
|
+
function targets(ast,ctx){
|
|
88
|
+
if(ast[0]==="term")return new Set([targetOf(ast[1],ctx)]);
|
|
89
|
+
if(ast[0]==="text")return new Set();
|
|
90
|
+
const s=new Set();ast.slice(1).forEach(a=>targets(a,ctx).forEach(x=>s.add(x)));return s;
|
|
91
|
+
}
|
|
92
|
+
const escRe=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
93
|
+
|
|
94
|
+
/* ---- compile to a row predicate ---- */
|
|
95
|
+
function termPred(target,f,op,value,ctx){
|
|
96
|
+
let get,kind;
|
|
97
|
+
if(target==="requests")[get,kind]=REQ[f];
|
|
98
|
+
else if(target==="edges")[get,kind]=EDGE(ctx)[f];
|
|
99
|
+
else if(f==="node.evidence"){
|
|
100
|
+
const ev=parseInt(value.toLowerCase().replace(/^(?:ev_|evd[ _]?)/,""),10);
|
|
101
|
+
if(!/^(?:ev_|evd[ _]?)?\d+$/i.test(value))fail("node.evidence needs an evidence id, got "+JSON.stringify(value));
|
|
102
|
+
if(op!=="eq"&&op!=="ne")fail("node.evidence supports eq and ne");
|
|
103
|
+
return n=>(n.evidence.includes(ev))===(op==="eq");
|
|
104
|
+
}else if(f==="node.role"){
|
|
105
|
+
if(op==="eq"||op==="ne"){const want=value.toLowerCase();
|
|
106
|
+
return n=>{const has=n.roles.some(r=>JSON.stringify(r).toLowerCase().includes('"'+want+'"'));return op==="eq"?has:!has;};}
|
|
107
|
+
const text=n=>JSON.stringify(n.roles);get=text;kind="text";
|
|
108
|
+
}else if(f.startsWith("node.attr.")){
|
|
109
|
+
const a=f.slice(10);if(!ATTR.test(a))fail("bad attribute name "+JSON.stringify(a));
|
|
110
|
+
get=n=>asText(n.attrs[a]);kind="text";
|
|
111
|
+
}else[get,kind]=NODE[f];
|
|
112
|
+
|
|
113
|
+
if(kind==="int"){
|
|
114
|
+
if(["cont","ncont","like","regex"].includes(op))fail(f+" is numeric; use eq ne gt gte lt lte");
|
|
115
|
+
if(!/^\s*[+-]?\d+\s*$/.test(value))fail(f+" needs a number, got "+JSON.stringify(value));
|
|
116
|
+
const v=parseInt(value,10);
|
|
117
|
+
return r=>{const x=get(r);if(x==null)return false;
|
|
118
|
+
return{eq:x===v,ne:x!==v,gt:x>v,gte:x>=v,lt:x<v,lte:x<=v}[op];};
|
|
119
|
+
}
|
|
120
|
+
if(["gt","gte","lt","lte"].includes(op))fail(f+" is text; "+op+" is for numbers");
|
|
121
|
+
const low=value.toLowerCase();
|
|
122
|
+
if(op==="eq")return r=>{const x=get(r);return x!=null&&String(x).toLowerCase()===low;};
|
|
123
|
+
if(op==="ne")return r=>{const x=get(r);return x==null||String(x).toLowerCase()!==low;};
|
|
124
|
+
if(op==="cont")return r=>{const x=get(r);return x!=null&&String(x).toLowerCase().includes(low);};
|
|
125
|
+
if(op==="ncont")return r=>{const x=get(r);return x==null||!String(x).toLowerCase().includes(low);};
|
|
126
|
+
if(op==="like"){const re=new RegExp("^"+value.split("*").map(escRe).join(".*")+"$","is");
|
|
127
|
+
return r=>{const x=get(r);return x!=null&&re.test(String(x));};}
|
|
128
|
+
let re;try{re=new RegExp(value,"i");}catch(e){fail("bad regex "+JSON.stringify(value)+": "+e.message);}
|
|
129
|
+
return r=>{const x=get(r);return x!=null&&re.test(String(x));};
|
|
130
|
+
}
|
|
131
|
+
function compile(ast,target,ctx){
|
|
132
|
+
const k=ast[0];
|
|
133
|
+
if(k==="and"){const a=compile(ast[1],target,ctx),b=compile(ast[2],target,ctx);return r=>a(r)&&b(r);}
|
|
134
|
+
if(k==="or"){const a=compile(ast[1],target,ctx),b=compile(ast[2],target,ctx);return r=>a(r)||b(r);}
|
|
135
|
+
if(k==="not"){const a=compile(ast[1],target,ctx);return r=>!a(r);}
|
|
136
|
+
if(k==="text"){const cols={requests:REQ_TEXT,nodes:NODE_TEXT,edges:EDGE_TEXT}[target],low=ast[1].toLowerCase();
|
|
137
|
+
return r=>cols.some(c=>r[c]!=null&&String(r[c]).toLowerCase().includes(low));}
|
|
138
|
+
return termPred(target,ast[1],ast[2],ast[3],ctx);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ---- data ---- */
|
|
142
|
+
function requestsFrom(evidence){
|
|
143
|
+
const hs=h=>(h||[]).map(([k,v])=>k+": "+v).join("\n");
|
|
144
|
+
return Object.values(evidence||{}).sort((a,b)=>a.id-b.id).map(e=>{
|
|
145
|
+
const rq=e.request||{},rs=e.response||{},parts=(rq.line||"").split(" ");
|
|
146
|
+
return{id:e.id,source:e.source,item:e.item,role:e.role,method:e.method,host:e.host,
|
|
147
|
+
path:parts.length>=2?parts[1]:"",path_template:e.path,status:e.status,mime:e.mime,
|
|
148
|
+
req_headers:hs(rq.headers),req_body:rq.body||"",resp_headers:hs(rs.headers),resp_body:rs.body||""};});
|
|
149
|
+
}
|
|
150
|
+
function context(db){
|
|
151
|
+
const nodeById=new Map(db.nodes.map(n=>[n.id,n]));
|
|
152
|
+
const out=new Map(),inc=new Map();
|
|
153
|
+
for(const e of db.edges){
|
|
154
|
+
if(!out.has(e.src))out.set(e.src,[]);out.get(e.src).push(e);
|
|
155
|
+
if(!inc.has(e.dst))inc.set(e.dst,[]);inc.get(e.dst).push(e);
|
|
156
|
+
}
|
|
157
|
+
return{db,nodeById,out,inc};
|
|
158
|
+
}
|
|
159
|
+
const evs=a=>(a||[]).map(i=>"ev_"+i);
|
|
160
|
+
const plural=t=>t+(t===1?"":"es");
|
|
161
|
+
|
|
162
|
+
/* ---- filters ---- */
|
|
163
|
+
function runFilter(ctx,text,limit){
|
|
164
|
+
const m=/\s+limit\s+(\d+)\s*$/i.exec(text);
|
|
165
|
+
if(m){limit=parseInt(m[1],10);text=text.slice(0,m.index);}
|
|
166
|
+
limit=Math.max(1,Math.min(limit,MAX_LIMIT));
|
|
167
|
+
const ast=parse(lex(text)),ts=targets(ast,ctx);
|
|
168
|
+
if(ts.size>1)fail("a query can filter one of requests, nodes or edges — this mixes "+[...ts].sort().join(", "));
|
|
169
|
+
const target=ts.size?[...ts][0]:"requests";
|
|
170
|
+
const pred=compile(ast,target,ctx);
|
|
171
|
+
let rows,cols,out;
|
|
172
|
+
if(target==="requests"){
|
|
173
|
+
rows=ctx.db.requests.filter(pred);
|
|
174
|
+
out=rows.slice(0,limit).map(r=>({ev:"ev_"+r.id,method:r.method,status:r.status,host:r.host,path:r.path,role:r.role||"",mime:r.mime||""}));
|
|
175
|
+
cols=["ev","method","status","host","path","role"];
|
|
176
|
+
}else if(target==="nodes"){
|
|
177
|
+
rows=ctx.db.nodes.filter(pred).sort((a,b)=>a.layer-b.layer||cmp(a.type,b.type)||cmp(a.label,b.label));
|
|
178
|
+
out=rows.slice(0,limit).map(n=>({id:n.id,type:n.type,layer:n.layer,label:n.label,source:n.source,state:n.attrs.api_state||"",roles:n.roles.join(","),evidence:evs(n.evidence)}));
|
|
179
|
+
cols=["type","label","state","source","roles","evidence"];
|
|
180
|
+
}else{
|
|
181
|
+
rows=ctx.db.edges.filter(pred);
|
|
182
|
+
const lab=id=>(ctx.nodeById.get(id)||{}).label;
|
|
183
|
+
out=rows.slice(0,limit).map(e=>({src:lab(e.src),type:e.type,state:e.state,dst:lab(e.dst),evidence:evs(e.evidence)}));
|
|
184
|
+
cols=["src","type","state","dst","evidence"];
|
|
185
|
+
}
|
|
186
|
+
const total=rows.length;
|
|
187
|
+
let note=total+" match"+(total!==1?"es":"");
|
|
188
|
+
if(total>out.length)note+=" (showing "+out.length+"; add `limit N`)";
|
|
189
|
+
return{kind:target,columns:cols,rows:out,note,total};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ---- graph verbs ---- */
|
|
193
|
+
function resolve(ctx,ref){
|
|
194
|
+
ref=ref.trim();
|
|
195
|
+
if(ctx.nodeById.has(ref))return ref;
|
|
196
|
+
const low=ref.toLowerCase();
|
|
197
|
+
let hits=ctx.db.nodes.filter(n=>String(n.label).toLowerCase()===low);
|
|
198
|
+
if(!hits.length)hits=ctx.db.nodes.filter(n=>String(n.label).toLowerCase().includes(low));
|
|
199
|
+
if(!hits.length)fail("no node matches "+JSON.stringify(ref));
|
|
200
|
+
if(hits.length>1)fail(JSON.stringify(ref)+" is ambiguous — be more specific: "+hits.slice(0,5).map(n=>n.label).join(" | "));
|
|
201
|
+
return hits[0].id;
|
|
202
|
+
}
|
|
203
|
+
const label=(ctx,id)=>(ctx.nodeById.get(id)||{label:id}).label;
|
|
204
|
+
function walk(ctx,nid,depth,dir){
|
|
205
|
+
depth=Math.max(1,Math.min(depth,8));
|
|
206
|
+
const adj=dir==="out"?ctx.out:ctx.inc,res=new Map();
|
|
207
|
+
let frontier=[nid];
|
|
208
|
+
for(let d=1;d<=depth;d++){
|
|
209
|
+
const next=[];
|
|
210
|
+
for(const cur of frontier)for(const e of adj.get(cur)||[]){
|
|
211
|
+
const n=dir==="out"?e.dst:e.src;
|
|
212
|
+
if(!res.has(n)){res.set(n,d);next.push(n);}
|
|
213
|
+
}
|
|
214
|
+
frontier=next;
|
|
215
|
+
}
|
|
216
|
+
const rows=[...res].filter(([id])=>ctx.nodeById.has(id)).map(([id,d])=>{const n=ctx.nodeById.get(id);return{hops:d,type:n.type,label:n.label,source:n.source};})
|
|
217
|
+
.sort((a,b)=>a.hops-b.hops||cmp(a.type,b.type)||cmp(a.label,b.label));
|
|
218
|
+
return{kind:"nodes",columns:["hops","type","label","source"],rows,
|
|
219
|
+
note:rows.length+" node(s) "+(dir==="out"?"downstream of":"upstream of")+" "+label(ctx,nid)+" (depth "+depth+")",total:rows.length};
|
|
220
|
+
}
|
|
221
|
+
function paths(ctx,start,goal,limit=3,maxDepth=8){
|
|
222
|
+
const back=goal===null,found=[],seen=new Map([[start,0]]);
|
|
223
|
+
const queue=[[start,[]]];
|
|
224
|
+
while(queue.length&&found.length<limit){
|
|
225
|
+
const [cur,trail]=queue.shift();
|
|
226
|
+
if(trail.length>=maxDepth)continue;
|
|
227
|
+
for(const e of ((back?ctx.inc:ctx.out).get(cur))||[]){
|
|
228
|
+
const nxt=back?e.src:e.dst;
|
|
229
|
+
const hop={from:e.src,to:e.dst,edge:e.type,state:e.state,evidence:evs(e.evidence)};
|
|
230
|
+
const path=trail.concat([hop]);
|
|
231
|
+
if(back?(ctx.nodeById.get(nxt)||{}).type==="host":nxt===goal){found.push(path);if(found.length>=limit)break;continue;}
|
|
232
|
+
if(!seen.has(nxt)||seen.get(nxt)>=path.length){seen.set(nxt,path.length);queue.push([nxt,path]);}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return back?found.map(p=>p.slice().reverse()):found;
|
|
236
|
+
}
|
|
237
|
+
function pathsResult(ctx,ps,title){
|
|
238
|
+
const rows=[];
|
|
239
|
+
ps.forEach((p,n)=>p.forEach((h,i)=>rows.push({path:n+1,hop:i+1,from:label(ctx,h.from),edge:h.edge,state:h.state,to:label(ctx,h.to),evidence:h.evidence})));
|
|
240
|
+
return{kind:"paths",columns:["path","hop","from","edge","state","to","evidence"],rows,note:ps.length?ps.length+" path(s): "+title:"no path: "+title,total:ps.length};
|
|
241
|
+
}
|
|
242
|
+
const HELP="BQL — filters and graph verbs over this model. Fields: req.method req.host req.path req.template req.header req.body resp.code resp.mime resp.header resp.body role id | node.type node.label node.id node.layer node.source node.role node.state node.evidence node.attr.<name> | edge.type edge.state edge.src edge.dst edge.src.type edge.dst.type edge.src.label edge.dst.label. Operators: eq ne cont ncont like regex gt gte lt lte. Combine with AND OR NOT ( ) and end with `limit N`. Verbs: reach <node> · blast <node> [depth N] · upstream <node> · neighbors <node> · path <a> to <b> · osint [section] · unknowns · stats · help.";
|
|
243
|
+
|
|
244
|
+
function run(db,text,limit=DEFAULT_LIMIT){
|
|
245
|
+
if(!db._ctx)Object.defineProperty(db,"_ctx",{value:context(db),enumerable:false});
|
|
246
|
+
const ctx=db._ctx;
|
|
247
|
+
text=(text||"").trim().replace(/;+\s*$/,"").trim();
|
|
248
|
+
if(!text)fail("empty query — try `help`");
|
|
249
|
+
const sp=text.search(/\s/),first=(sp<0?text:text.slice(0,sp)).toLowerCase(),rest=sp<0?"":text.slice(sp).trim();
|
|
250
|
+
if(first==="help")return{kind:"text",columns:[],rows:[],note:HELP,total:null};
|
|
251
|
+
if(first==="stats"){const s=db.stats||{};return{kind:"table",columns:["metric","value"],rows:Object.entries(s).map(([metric,value])=>({metric,value})),note:"graph.db",total:Object.keys(s).length};}
|
|
252
|
+
if(first==="unknowns"){const rows=db.unknowns.map(u=>({type:u.type,entity:u.entity,we_dont_know:u.we_dont_know,next_step:u.next_step}));
|
|
253
|
+
return{kind:"table",columns:["type","entity","we_dont_know","next_step"],rows,note:rows.length+" unknown(s)",total:rows.length};}
|
|
254
|
+
if(first==="osint"){
|
|
255
|
+
if(!db.osint)return{kind:"facts",columns:["section","key","value"],rows:[],note:"no OSINT stored yet — build with OSINT enabled (the default)",total:0};
|
|
256
|
+
const sec=rest.toLowerCase(),rows=db.facts.filter(f=>!rest||f.section.toLowerCase()===sec);
|
|
257
|
+
return{kind:"facts",columns:["section","key","value"],rows,note:"OSINT run #"+db.osint.id+" · "+db.osint.host+" · "+db.osint.generated_at,total:rows.length};}
|
|
258
|
+
if(["schema","sql","provenance","static-only","changed-since","reaches-privileged"].includes(first))fail(first+" is available in the CLI (`proxy2model q`), not in the report");
|
|
259
|
+
if(["reach","blast","upstream","neighbors","path"].includes(first)){
|
|
260
|
+
if(!rest)fail(first+" needs a node");
|
|
261
|
+
if(first==="path"){const m=rest.split(/\s+to\s+/i);if(m.length<2)fail("usage: path <a> to <b>");
|
|
262
|
+
const a=resolve(ctx,unquote(m[0].trim())),b=resolve(ctx,unquote(m.slice(1).join(" to ").trim()));
|
|
263
|
+
return pathsResult(ctx,paths(ctx,a,b,1),label(ctx,a)+" → "+label(ctx,b));}
|
|
264
|
+
let words=rest.split(/\s+/),depth=3;
|
|
265
|
+
if(words.length>=2&&words[words.length-2].toLowerCase()==="depth"&&/^\d+$/.test(words[words.length-1])){depth=parseInt(words.pop(),10);words.pop();}
|
|
266
|
+
const nid=resolve(ctx,unquote(words.join(" ")));
|
|
267
|
+
if(first==="reach")return pathsResult(ctx,paths(ctx,nid,null),"how "+label(ctx,nid)+" is reached");
|
|
268
|
+
if(first==="blast")return walk(ctx,nid,depth,"out");
|
|
269
|
+
if(first==="upstream")return walk(ctx,nid,depth,"in");
|
|
270
|
+
const d=walk(ctx,nid,depth,"out"),u=walk(ctx,nid,depth,"in");
|
|
271
|
+
const rows=d.rows.map(r=>Object.assign({direction:"out"},r)).concat(u.rows.map(r=>Object.assign({direction:"in"},r)));
|
|
272
|
+
return{kind:"nodes",columns:["direction","hops","type","label","source"],rows,note:rows.length+" node(s) within "+depth+" hop(s) of "+label(ctx,nid),total:rows.length};
|
|
273
|
+
}
|
|
274
|
+
return runFilter(ctx,text,limit);
|
|
275
|
+
}
|
|
276
|
+
return{run,requestsFrom,BQLError,HELP};
|
|
277
|
+
})();
|
|
278
|
+
"""
|