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/cli/app.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""The argument parser and the entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .. import __version__
|
|
10
|
+
from ..core.context import LENSES
|
|
11
|
+
from ..db import bql
|
|
12
|
+
from . import ui
|
|
13
|
+
from .history import cmd_builds, cmd_diff, cmd_watch
|
|
14
|
+
from .common import (
|
|
15
|
+
_name,
|
|
16
|
+
)
|
|
17
|
+
from .build import (
|
|
18
|
+
cmd_build, cmd_crawl,
|
|
19
|
+
)
|
|
20
|
+
from .analyze import (
|
|
21
|
+
cmd_q, cmd_query, cmd_crossrole, cmd_osint, cmd_graph, cmd_methodology, cmd_gaps, cmd_changes, cmd_falsify, cmd_openapi, cmd_animate,
|
|
22
|
+
)
|
|
23
|
+
from .mcp import cmd_mcp
|
|
24
|
+
from .browse import cmd_browse
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# What a researcher reaches for every day; everything else is one line under "More".
|
|
28
|
+
COMMANDS = [
|
|
29
|
+
("build", "build the model from a Burp XML, Logger++ CSV, HAR or Caido export"),
|
|
30
|
+
("browse", "open a browser, record what you do in it, build the model as you go"),
|
|
31
|
+
("crawl", "build the model by crawling a running app (authorized targets only)"),
|
|
32
|
+
("query", "ask the model a question, answered with evidence and no AI"),
|
|
33
|
+
("diff", "what changed between two builds"),
|
|
34
|
+
("openapi", "print the observed API as OpenAPI 3.0"),
|
|
35
|
+
]
|
|
36
|
+
MORE = ["cross-role", "builds", "watch", "q", "graph", "gaps", "changes", "mcp",
|
|
37
|
+
"methodology", "falsify", "osint", "animate"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def overview() -> str:
|
|
41
|
+
"""The top-level help: the everyday commands, then the rest on one line."""
|
|
42
|
+
w = max(len(c) for c, _ in COMMANDS)
|
|
43
|
+
out = [ui.title(__version__), "Captured proxy traffic in, an evidence-backed model of the web app out.", "",
|
|
44
|
+
f"{ui.style('usage', 'dim')} proxy2model <command> [options]",
|
|
45
|
+
f" proxy2model FILE ... {ui.style('same as build FILE', 'dim')}", ""]
|
|
46
|
+
out += [f" {c.ljust(w)} {d}" for c, d in COMMANDS]
|
|
47
|
+
out += ["", f" {ui.style('More:', 'dim')} {', '.join(MORE)}", "",
|
|
48
|
+
ui.style("proxy2model <command> --help shows what a command does and its options.", "dim")]
|
|
49
|
+
return "\n".join(out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main(argv=None) -> int:
|
|
53
|
+
p = argparse.ArgumentParser(
|
|
54
|
+
prog="proxy2model",
|
|
55
|
+
description="Turn captured proxy traffic (Burp, Caido, HAR, Logger++) into an evidence-backed web-app model.",
|
|
56
|
+
)
|
|
57
|
+
p.add_argument("--version", action="version", version=f"proxy2model {__version__}")
|
|
58
|
+
p.set_defaults(func=None)
|
|
59
|
+
subparsers = p.add_subparsers(dest="cmd", metavar="<command>")
|
|
60
|
+
|
|
61
|
+
class _Sub: # every command's --help opens with what it does
|
|
62
|
+
@staticmethod
|
|
63
|
+
def add_parser(name, **kw):
|
|
64
|
+
kw.setdefault("description", kw.get("help"))
|
|
65
|
+
return subparsers.add_parser(name, **kw)
|
|
66
|
+
sub = _Sub()
|
|
67
|
+
|
|
68
|
+
b = sub.add_parser("build", help="build the model (also the default: `proxy2model file.xml`)")
|
|
69
|
+
b.add_argument("log", help="Burp 'Save items' XML, Logger++ CSV, HAR or Caido JSON/CSV export to build from")
|
|
70
|
+
b.add_argument("--webapp", "-w", default="app", type=_name("--webapp"),
|
|
71
|
+
help="name for this app's model (default: app)")
|
|
72
|
+
b.add_argument("--role", type=_name("--role"),
|
|
73
|
+
help="tag this capture with a role; role builds into the same "
|
|
74
|
+
"--out/--webapp are merged into one model")
|
|
75
|
+
b.add_argument("--fresh", action="store_true",
|
|
76
|
+
help="discard previously merged role inputs first")
|
|
77
|
+
b.add_argument("--scope", action="append",
|
|
78
|
+
help="first-party host or domain (repeatable or comma-separated); "
|
|
79
|
+
"default: the registrable domain of the busiest host")
|
|
80
|
+
b.add_argument("--lens", choices=LENSES, default="attention",
|
|
81
|
+
help="ordering/filter for context.json")
|
|
82
|
+
b.add_argument("--cap", type=int, default=40, metavar="N",
|
|
83
|
+
help="max items per section in context.json (default: 40)")
|
|
84
|
+
b.add_argument("--out", default="proxy2model-out",
|
|
85
|
+
help="output directory (default: proxy2model-out)")
|
|
86
|
+
b.add_argument("--screenshot", metavar="FILE",
|
|
87
|
+
help="a PNG, JPEG or WebP of the app to show on the report overview "
|
|
88
|
+
"(pixels are not masked: check it first)")
|
|
89
|
+
b.add_argument("--label", metavar="TEXT",
|
|
90
|
+
help="a name for this build in the history (shown by `builds`, usable in `diff`)")
|
|
91
|
+
b.add_argument("--osint", action="store_true",
|
|
92
|
+
help="also run light external recon on the primary host (off by default; "
|
|
93
|
+
"sends DNS/TLS/HTTP requests; PROXY2MODEL_OFFLINE=1 forces it off)")
|
|
94
|
+
b.add_argument("--osint-timeout", type=float, default=5.0, metavar="SEC",
|
|
95
|
+
help="per-lookup timeout for the build's OSINT (default: 5)")
|
|
96
|
+
b.add_argument("--skip-tools", metavar="TOOLS",
|
|
97
|
+
help="Logger++ CSV: drop rows from these Burp tools (comma-separated), e.g. Scanner; "
|
|
98
|
+
"Caido: drop these sources, e.g. automate")
|
|
99
|
+
b.add_argument("--only-tools", metavar="TOOLS",
|
|
100
|
+
help="Logger++ CSV: keep only rows from these Burp tools, e.g. Proxy,Extensions; "
|
|
101
|
+
"Caido: keep only these sources, e.g. intercept")
|
|
102
|
+
b.add_argument("--animate", action="store_true",
|
|
103
|
+
help="play the build animation in the terminal")
|
|
104
|
+
b.add_argument("--no-type", action="store_true",
|
|
105
|
+
help="animation: skip the typing effect")
|
|
106
|
+
b.set_defaults(func=cmd_build)
|
|
107
|
+
|
|
108
|
+
cw = sub.add_parser("crawl", help="crawl a running web app and build the model from it, "
|
|
109
|
+
"no proxy needed; authorized targets only")
|
|
110
|
+
cw.add_argument("url", help="start URL, e.g. https://shop.example.com/")
|
|
111
|
+
cw.add_argument("--yes", action="store_true", help="confirm you are authorized to test this target")
|
|
112
|
+
cw.add_argument("--webapp", "-w", default="app", type=_name("--webapp"),
|
|
113
|
+
help="name for this app's model (default: app)")
|
|
114
|
+
cw.add_argument("--role", type=_name("--role"),
|
|
115
|
+
help="tag this crawl with a role; crawls into the same --out/--webapp merge")
|
|
116
|
+
cw.add_argument("--fresh", action="store_true", help="discard previously merged role inputs first")
|
|
117
|
+
cw.add_argument("--scope", action="append",
|
|
118
|
+
help="extra in-scope hosts (host or host:port; repeatable or comma-separated). "
|
|
119
|
+
"The crawl never leaves the start origin plus these")
|
|
120
|
+
cw.add_argument("--include-subdomains", action="store_true",
|
|
121
|
+
help="also crawl subdomains of the start host's registrable domain")
|
|
122
|
+
cw.add_argument("--max-requests", type=int, default=3000, metavar="N",
|
|
123
|
+
help="request budget; 0 = no limit (default: 3000)")
|
|
124
|
+
cw.add_argument("--max-assets", type=int, default=2000, metavar="N",
|
|
125
|
+
help="browser mode: cap on image/font/media loads (0 = no cap), so they don't "
|
|
126
|
+
"starve page and API budget on an asset-heavy site (default: 2000)")
|
|
127
|
+
cw.add_argument("--depth", type=int, default=12, help="link depth from the start page (default: 12)")
|
|
128
|
+
cw.add_argument("--max-seconds", type=float, default=0, metavar="SEC",
|
|
129
|
+
help="wall-clock limit for the whole crawl; 0 = none")
|
|
130
|
+
cw.add_argument("--delay", type=float, default=0.1, metavar="SEC",
|
|
131
|
+
help="static mode: minimum seconds between requests (default: 0.1)")
|
|
132
|
+
cw.add_argument("--threads", type=int, default=4, help="static mode: parallel fetches, 1-8 (default: 4)")
|
|
133
|
+
cw.add_argument("--per-template-cap", type=int, default=25, metavar="N",
|
|
134
|
+
help="max variants of one path template, e.g. /item/{id} (default: 25)")
|
|
135
|
+
cw.add_argument("--browser", choices=["auto", "on", "off"], default="auto",
|
|
136
|
+
help="crawl with a real Chrome/Chromium so JavaScript runs: auto (default) uses one "
|
|
137
|
+
"if installed, on requires it, off is the static crawler")
|
|
138
|
+
cw.add_argument("--chrome", metavar="PATH", help="Chrome/Chromium binary (else found automatically, "
|
|
139
|
+
"or PROXY2MODEL_CHROME)")
|
|
140
|
+
cw.add_argument("--max-clicks", type=int, default=25, metavar="N",
|
|
141
|
+
help="browser: clicks per page state (default: 25)")
|
|
142
|
+
cw.add_argument("--no-interact", action="store_true", help="browser: load pages, do not click or fill")
|
|
143
|
+
cw.add_argument("--no-forms", action="store_true", help="browser: fill fields but do not submit forms")
|
|
144
|
+
cw.add_argument("--read-only", action="store_true",
|
|
145
|
+
help="browser: block every request that is not GET/HEAD/OPTIONS")
|
|
146
|
+
cw.add_argument("--headful", action="store_true", help="browser: show the window (debugging)")
|
|
147
|
+
cw.add_argument("--journey", action="append", metavar="FILE",
|
|
148
|
+
help="run a scripted journey (JSON: login, register, checkout, uploads …) before the "
|
|
149
|
+
"crawl, in the same session; repeatable. See the docs for the format")
|
|
150
|
+
cw.add_argument("--var", action="append", metavar="name=value",
|
|
151
|
+
help="a variable for journeys, as ${name} (use ${env:NAME} for secrets)")
|
|
152
|
+
cw.add_argument("--journey-only", action="store_true",
|
|
153
|
+
help="run the journeys and skip the autonomous crawl")
|
|
154
|
+
cw.add_argument("--auth-storage", metavar="KEY",
|
|
155
|
+
help="browser: after --auth-login, store the token in localStorage[KEY] and cookie KEY "
|
|
156
|
+
"(what many SPAs read), e.g. token")
|
|
157
|
+
cw.add_argument("--local-storage", action="append", metavar="key=value",
|
|
158
|
+
help="browser: set a localStorage value before the app loads (repeatable)")
|
|
159
|
+
cw.add_argument("--timeout", type=float, default=10.0, help="per-request timeout in seconds")
|
|
160
|
+
cw.add_argument("--header", action="append", metavar="'Name: value'",
|
|
161
|
+
help="extra request header, e.g. 'Authorization: Bearer ...' (repeatable)")
|
|
162
|
+
cw.add_argument("--cookie", metavar="'a=b; c=d'", help="initial cookies, to crawl as a logged-in user")
|
|
163
|
+
cw.add_argument("--auth-login", metavar="URL",
|
|
164
|
+
help="POST credentials here first and carry the token (see --auth-body, --auth-token)")
|
|
165
|
+
cw.add_argument("--auth-body", metavar="JSON|@FILE", help="JSON body for --auth-login")
|
|
166
|
+
cw.add_argument("--auth-token", metavar="PATH",
|
|
167
|
+
help="dotted path to the token in the login reply, e.g. authentication.token")
|
|
168
|
+
cw.add_argument("--auth-scheme", default="Bearer", help="Authorization scheme for the token (default: Bearer)")
|
|
169
|
+
cw.add_argument("--no-probe-js", action="store_true",
|
|
170
|
+
help="do not request the GET endpoints that scripts and pages name")
|
|
171
|
+
cw.add_argument("--submit-forms", action="store_true",
|
|
172
|
+
help="also POST forms with benign sample values (never forms with a password field)")
|
|
173
|
+
cw.add_argument("--respect-robots", action="store_true", help="skip paths robots.txt disallows")
|
|
174
|
+
cw.add_argument("--insecure", action="store_true", help="do not verify TLS certificates")
|
|
175
|
+
cw.add_argument("--save-xml", metavar="PATH",
|
|
176
|
+
help="also write the crawl as a Burp 'Save items' XML (contains raw values)")
|
|
177
|
+
cw.add_argument("--lens", choices=LENSES, default="attention", help="ordering/filter for context.json")
|
|
178
|
+
cw.add_argument("--cap", type=int, default=40, metavar="N", help="max items per section in context.json")
|
|
179
|
+
cw.add_argument("--out", default="proxy2model-out", help="output directory (default: proxy2model-out)")
|
|
180
|
+
cw.add_argument("--animate", action="store_true", help="play the build animation in the terminal")
|
|
181
|
+
cw.add_argument("--no-type", action="store_true", help="animation: skip the typing effect")
|
|
182
|
+
cw.add_argument("--screenshot", metavar="FILE",
|
|
183
|
+
help="a PNG, JPEG or WebP of the app to show on the report overview "
|
|
184
|
+
"(pixels are not masked: check it first)")
|
|
185
|
+
cw.add_argument("--label", metavar="TEXT",
|
|
186
|
+
help="a name for this build in the history (shown by `builds`, usable in `diff`)")
|
|
187
|
+
cw.add_argument("--no-screenshot", action="store_true",
|
|
188
|
+
help="do not capture the start page (an anonymous browser crawl does by default)")
|
|
189
|
+
cw.add_argument("--osint", action="store_true",
|
|
190
|
+
help="also run light external recon on the primary host (off by default)")
|
|
191
|
+
cw.add_argument("--osint-timeout", type=float, default=5.0, metavar="SEC")
|
|
192
|
+
cw.set_defaults(func=cmd_crawl)
|
|
193
|
+
|
|
194
|
+
q = sub.add_parser("query", help="ask the model a factual question (no AI); try 'help'")
|
|
195
|
+
q.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
196
|
+
q.add_argument("question", help="the question; `help` lists what can be asked")
|
|
197
|
+
q.add_argument("--out", default="proxy2model-out",
|
|
198
|
+
help="output directory used at build time")
|
|
199
|
+
q.set_defaults(func=cmd_query)
|
|
200
|
+
|
|
201
|
+
bq = sub.add_parser("q", help="query graph.db with BQL (HTTPQL-style filters + graph "
|
|
202
|
+
"verbs); no query opens an interactive prompt")
|
|
203
|
+
bq.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
204
|
+
bq.add_argument("query", nargs="*", help="a BQL query; `help` prints the language")
|
|
205
|
+
bq.add_argument("--format", choices=["table", "json"], default="table")
|
|
206
|
+
bq.add_argument("--limit", type=int, default=bql.DEFAULT_LIMIT, help="max rows (default: 100)")
|
|
207
|
+
bq.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
208
|
+
bq.set_defaults(func=cmd_q)
|
|
209
|
+
|
|
210
|
+
cr = sub.add_parser("cross-role", help="compare what two roles reached")
|
|
211
|
+
cr.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
212
|
+
cr.add_argument("--low", required=True, type=_name("--low"),
|
|
213
|
+
help="the less-privileged role (as tagged with --role)")
|
|
214
|
+
cr.add_argument("--high", required=True, type=_name("--high"),
|
|
215
|
+
help="the more-privileged role (as tagged with --role)")
|
|
216
|
+
cr.add_argument("--json", action="store_true", help="print JSON instead of text")
|
|
217
|
+
cr.add_argument("--out", default="proxy2model-out",
|
|
218
|
+
help="output directory used at build time")
|
|
219
|
+
cr.set_defaults(func=cmd_crossrole)
|
|
220
|
+
|
|
221
|
+
o = sub.add_parser("osint", help="external recon for a host (DNS, TLS, headers, tech, "
|
|
222
|
+
"email, subdomains, RDAP); authorized targets only")
|
|
223
|
+
o.add_argument("host")
|
|
224
|
+
o.add_argument("--webapp", "-w", type=_name("--webapp"),
|
|
225
|
+
help="save osint.json into OUT/WEBAPP/ so build folds it into context.json")
|
|
226
|
+
o.add_argument("--yes", action="store_true", help="confirm you are authorized to test this host")
|
|
227
|
+
o.add_argument("--ports", action="store_true", help="also TCP-connect common ports")
|
|
228
|
+
o.add_argument("--no-subdomains", action="store_true", help="skip certificate-transparency lookup")
|
|
229
|
+
o.add_argument("--timeout", type=float, default=6.0,
|
|
230
|
+
help="per-lookup timeout in seconds (default: 6)")
|
|
231
|
+
o.add_argument("--out", default="proxy2model-out",
|
|
232
|
+
help="output directory (default: proxy2model-out)")
|
|
233
|
+
o.add_argument("--json", action="store_true", help="print JSON instead of the text summary")
|
|
234
|
+
o.add_argument("--out-file", help="write the JSON report to this path")
|
|
235
|
+
o.set_defaults(func=cmd_osint)
|
|
236
|
+
|
|
237
|
+
g = sub.add_parser("graph", help="reason over the graph, or export it "
|
|
238
|
+
"(GraphML / Cypher / JSON) for tooling and AI")
|
|
239
|
+
g.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
240
|
+
g.add_argument("--format", choices=["overview", "reason", "json", "graphml", "cypher"],
|
|
241
|
+
default="overview",
|
|
242
|
+
help="overview (text), reason (graph+reasoning JSON for an LLM), "
|
|
243
|
+
"json (node-link), graphml (Gephi/yEd), cypher (Neo4j)")
|
|
244
|
+
g.add_argument("--reach", metavar="NODE",
|
|
245
|
+
help="show how a node is reached (entry-point paths)")
|
|
246
|
+
g.add_argument("--blast", metavar="NODE",
|
|
247
|
+
help="show what a node touches downstream")
|
|
248
|
+
g.add_argument("--touched-by", metavar="NODE",
|
|
249
|
+
help="show what leads into a node upstream")
|
|
250
|
+
g.add_argument("--depth", type=int, default=3, help="hops for --blast/--touched-by")
|
|
251
|
+
g.add_argument("--out-file", help="write the output to this path")
|
|
252
|
+
g.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
253
|
+
g.set_defaults(func=cmd_graph)
|
|
254
|
+
|
|
255
|
+
gp = sub.add_parser("gaps", help="rank the model's open questions by how much "
|
|
256
|
+
"resolving each improves the picture")
|
|
257
|
+
gp.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
258
|
+
gp.add_argument("--json", action="store_true", help="print JSON")
|
|
259
|
+
gp.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
260
|
+
gp.set_defaults(func=cmd_gaps)
|
|
261
|
+
|
|
262
|
+
bw = sub.add_parser("browse", help="open a browser and build the model from what you do in it; "
|
|
263
|
+
"asks for the URL, scope and mode when they are not given")
|
|
264
|
+
bw.add_argument("url", nargs="?", help="the app's start URL")
|
|
265
|
+
bw.add_argument("--webapp", "-w", type=_name("--webapp"),
|
|
266
|
+
help="name for this app's model (default: from the domain)")
|
|
267
|
+
bw.add_argument("--scope", action="append",
|
|
268
|
+
help="what to record: host, host:port or *.domain (repeatable or comma-separated; "
|
|
269
|
+
"default: *.<the URL's domain>)")
|
|
270
|
+
mode = bw.add_mutually_exclusive_group()
|
|
271
|
+
mode.add_argument("--active", dest="active", action="store_true", default=None,
|
|
272
|
+
help="also crawl the app in a background tab, sharing your login (authorized targets only)")
|
|
273
|
+
mode.add_argument("--passive", dest="active", action="store_false",
|
|
274
|
+
help="record only what you do (the default)")
|
|
275
|
+
bw.add_argument("--yes", action="store_true",
|
|
276
|
+
help="ask nothing: you are authorized to test the target; defaults fill the rest")
|
|
277
|
+
bw.add_argument("--role", type=_name("--role"), help="tag this session's traffic with a role")
|
|
278
|
+
bw.add_argument("--fresh", action="store_true",
|
|
279
|
+
help="discard every saved input (earlier builds and sessions) first")
|
|
280
|
+
bw.add_argument("--allow-writes", action="store_true",
|
|
281
|
+
help="active crawl: also let through the POST/PUT/DELETE the app itself sends "
|
|
282
|
+
"(by default the crawl tab sends GET only, since it shares your login)")
|
|
283
|
+
bw.add_argument("--keep-login", dest="keep_login", action="store_true", default=None,
|
|
284
|
+
help="keep the browser profile (and its login) for the next session; it holds real "
|
|
285
|
+
"cookies and lives in ~/.config/proxy2model/profiles/, outside the output")
|
|
286
|
+
bw.add_argument("--no-open", dest="open", action="store_false", help="do not open the report when done")
|
|
287
|
+
bw.add_argument("--max-requests", type=int, default=3000, metavar="N",
|
|
288
|
+
help="active crawl: stop after N requests (default: 3000; 0 = no limit)")
|
|
289
|
+
bw.add_argument("--max-seconds", type=float, default=0, metavar="SEC", help="active crawl: time limit")
|
|
290
|
+
bw.add_argument("--chrome", metavar="PATH", help="the Chrome/Chromium/Edge/Brave to use (default: found)")
|
|
291
|
+
bw.add_argument("--insecure", action="store_true", help="accept invalid TLS certificates")
|
|
292
|
+
bw.add_argument("--headless", action="store_true", help=argparse.SUPPRESS) # tests
|
|
293
|
+
bw.add_argument("--interval", type=float, default=5.0, metavar="SEC",
|
|
294
|
+
help="rebuild at most this often while new traffic arrives (default: 5)")
|
|
295
|
+
bw.add_argument("--lens", choices=LENSES, default="attention", help="ordering/filter for context.json")
|
|
296
|
+
bw.add_argument("--cap", type=int, default=40, metavar="N", help="max items per section in context.json")
|
|
297
|
+
bw.add_argument("--label", metavar="TEXT", help="name for this session's build in the history (default: browse)")
|
|
298
|
+
bw.add_argument("--out", default="proxy2model-out", help="output directory (default: proxy2model-out)")
|
|
299
|
+
bw.set_defaults(func=cmd_browse)
|
|
300
|
+
|
|
301
|
+
mc = sub.add_parser("mcp", help="serve the built models to an AI agent over MCP (stdio); read-only")
|
|
302
|
+
mc.add_argument("--out", default="proxy2model-out",
|
|
303
|
+
help="output directory holding the models (default: proxy2model-out); give an absolute path")
|
|
304
|
+
mc.set_defaults(func=cmd_mcp)
|
|
305
|
+
|
|
306
|
+
oa = sub.add_parser("openapi", help="export the observed API as an OpenAPI 3.0 document")
|
|
307
|
+
oa.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
308
|
+
oa.add_argument("-o", "--output", metavar="FILE", help="write here instead of stdout")
|
|
309
|
+
oa.add_argument("--host", help="only endpoints on this host")
|
|
310
|
+
oa.add_argument("--include-static", action="store_true",
|
|
311
|
+
help="also include endpoints the client code references but the capture never reached")
|
|
312
|
+
oa.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
313
|
+
oa.set_defaults(func=cmd_openapi)
|
|
314
|
+
|
|
315
|
+
ch = sub.add_parser("changes", help="diff this model against an older one, "
|
|
316
|
+
"grouped by feature area (drift, never findings)")
|
|
317
|
+
ch.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
318
|
+
ch.add_argument("--against", metavar="OLD_MODEL_JSON",
|
|
319
|
+
help="path to an earlier model.json to compare against")
|
|
320
|
+
ch.add_argument("--against-build", metavar="ID|previous",
|
|
321
|
+
help="compare against a build stored in graph.db (see `q shop "
|
|
322
|
+
"\"sql SELECT id, built_at FROM builds\"`), or `previous`")
|
|
323
|
+
ch.add_argument("--json", action="store_true", help="print JSON")
|
|
324
|
+
ch.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
325
|
+
ch.set_defaults(func=cmd_changes)
|
|
326
|
+
|
|
327
|
+
df = sub.add_parser("diff", help="what changed between two builds of an app: new workflows, "
|
|
328
|
+
"changed fingerprints, gone things (drift, never findings)")
|
|
329
|
+
df.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
330
|
+
df.add_argument("a", nargs="?", metavar="FROM",
|
|
331
|
+
help="the older build: an id, `previous`, `first` or a label (default: previous)")
|
|
332
|
+
df.add_argument("b", nargs="?", metavar="TO",
|
|
333
|
+
help="the newer build (default: latest)")
|
|
334
|
+
df.add_argument("--json", action="store_true", help="print JSON")
|
|
335
|
+
df.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
336
|
+
df.set_defaults(func=cmd_diff)
|
|
337
|
+
|
|
338
|
+
wa = sub.add_parser("watch", help="watch a folder of exports: each new export becomes a build, "
|
|
339
|
+
"and the change from the last one is printed")
|
|
340
|
+
wa.add_argument("folder", help="folder exports (Burp Save items XML, Logger++ CSV, HAR or Caido) are saved into")
|
|
341
|
+
wa.add_argument("--webapp", "-w", required=True, type=_name("webapp"), help="name for the model")
|
|
342
|
+
wa.add_argument("--interval", type=float, default=5.0, metavar="SEC", help="seconds between checks (default: 5)")
|
|
343
|
+
wa.add_argument("--once", action="store_true", help="process what is there and exit instead of watching")
|
|
344
|
+
wa.add_argument("--skip-existing", action="store_true",
|
|
345
|
+
help="only react to exports that appear after this starts")
|
|
346
|
+
wa.add_argument("--pattern", default="*.xml,*.csv,*.har,*.json",
|
|
347
|
+
help="file patterns to pick up (default: *.xml,*.csv,*.har,*.json)")
|
|
348
|
+
wa.add_argument("--role", help="tag each export with this role (merges with other roles' inputs)")
|
|
349
|
+
wa.add_argument("--scope", action="append", help="in-scope host (repeatable), as for build")
|
|
350
|
+
wa.add_argument("--skip-tools", metavar="TOOLS", help="Logger++ CSV: drop rows from these Burp tools (Caido: these sources)")
|
|
351
|
+
wa.add_argument("--osint", action="store_true", help="also run light external recon on each build")
|
|
352
|
+
wa.add_argument("--out", default="proxy2model-out", help="output directory (default: proxy2model-out)")
|
|
353
|
+
wa.set_defaults(func=cmd_watch)
|
|
354
|
+
|
|
355
|
+
bl = sub.add_parser("builds", help="list the builds kept in graph.db, to pick two to diff")
|
|
356
|
+
bl.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
357
|
+
bl.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
358
|
+
bl.set_defaults(func=cmd_builds)
|
|
359
|
+
|
|
360
|
+
fa = sub.add_parser("falsify", help="emit an AI prompt that stress-tests the "
|
|
361
|
+
"scaffold's hypotheses (tries to eliminate them)")
|
|
362
|
+
fa.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
363
|
+
fa.add_argument("--out-file", help="write the prompt to this path")
|
|
364
|
+
fa.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
365
|
+
fa.set_defaults(func=cmd_falsify)
|
|
366
|
+
|
|
367
|
+
me = sub.add_parser("methodology", help="a target-specific recon & hunting "
|
|
368
|
+
"scaffold, or the AI prompt that turns it "
|
|
369
|
+
"into a methodology (no generic checklist)")
|
|
370
|
+
me.add_argument("webapp", type=_name("webapp"), help="model name used at build time")
|
|
371
|
+
me.add_argument("--prompt", action="store_true",
|
|
372
|
+
help="emit the full AI-ready prompt + model to paste into a chat model")
|
|
373
|
+
me.add_argument("--json", action="store_true", help="the scaffold as JSON")
|
|
374
|
+
me.add_argument("--out-file", help="write the output to this path")
|
|
375
|
+
me.add_argument("--out", default="proxy2model-out", help="output directory used at build time")
|
|
376
|
+
me.set_defaults(func=cmd_methodology)
|
|
377
|
+
|
|
378
|
+
an = sub.add_parser("animate", help="render the build animation (terminal or --svg)")
|
|
379
|
+
an.add_argument("--webapp", "-w", default="app", type=_name("--webapp"),
|
|
380
|
+
help="model name used at build time")
|
|
381
|
+
an.add_argument("--log", help="build fresh from this export instead of a saved model")
|
|
382
|
+
an.add_argument("--svg", help="write an animated SVG to this path")
|
|
383
|
+
an.add_argument("--out", default="proxy2model-out",
|
|
384
|
+
help="output directory used at build time")
|
|
385
|
+
an.add_argument("--no-type", action="store_true", help="skip the typing effect")
|
|
386
|
+
an.set_defaults(func=cmd_animate)
|
|
387
|
+
|
|
388
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
389
|
+
known = {"build", "crawl", "query", "q", "cross-role", "osint", "graph", "methodology",
|
|
390
|
+
"gaps", "openapi", "browse", "mcp", "changes", "diff", "builds", "watch", "falsify", "animate", "-h", "--help", "--version"}
|
|
391
|
+
# bare `proxy2model file.xml ...` → treat as build, but only when the first
|
|
392
|
+
# word is a real file; a typo'd subcommand gets a clear error instead
|
|
393
|
+
if argv and argv[0] not in known and not argv[0].startswith("-"):
|
|
394
|
+
if not os.path.exists(argv[0]):
|
|
395
|
+
print(ui.error(f"{argv[0]!r} is not a command or an existing file."), file=sys.stderr)
|
|
396
|
+
print("run `proxy2model` to list the commands", file=sys.stderr)
|
|
397
|
+
return 2
|
|
398
|
+
argv = ["build"] + argv
|
|
399
|
+
|
|
400
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
401
|
+
print(overview())
|
|
402
|
+
return 0 if argv else 1
|
|
403
|
+
args = p.parse_args(argv)
|
|
404
|
+
|
|
405
|
+
if getattr(args, "func", None) is None:
|
|
406
|
+
print(overview())
|
|
407
|
+
return 1
|
|
408
|
+
return args.func(args)
|