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,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
From a model to a methodology.
|
|
3
|
+
|
|
4
|
+
A generic checklist ("test for IDOR, check CORS, …") ignores the app in front
|
|
5
|
+
of you. This module turns the model into the opposite: a target-specific
|
|
6
|
+
*scaffold* for a recon-and-hunting methodology, derived from what the capture
|
|
7
|
+
actually shows — the app's feature areas, how each is reached, its trust posture,
|
|
8
|
+
what the capture could not answer — every line cited to a real request.
|
|
9
|
+
|
|
10
|
+
The scaffold is deterministic and honest. It is not the methodology itself: it
|
|
11
|
+
is the grounded structure an AI reasons over to produce one — sequencing the
|
|
12
|
+
work, connecting areas, and going deeper than any list could. `methodology_prompt`
|
|
13
|
+
packages the scaffold and the graph with instructions that push the model toward
|
|
14
|
+
a plan built from *this* app's evidence, not a template.
|
|
15
|
+
|
|
16
|
+
Nothing here is a finding or a payload. Each line of inquiry ends in an
|
|
17
|
+
observation or a two-account comparison to make in an authorised session — the
|
|
18
|
+
question that would advance understanding, not an exploit.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .graph import ReasonGraph
|
|
24
|
+
from .model import PRIV_RE, Model
|
|
25
|
+
|
|
26
|
+
# How much each structural signal raises an area's priority for *understanding*
|
|
27
|
+
# (not risk): an unwalked path teaches the most, a privileged path matters, an
|
|
28
|
+
# ambiguous auth boundary is worth resolving.
|
|
29
|
+
_WEIGHT = {
|
|
30
|
+
"static_only": 3, # referenced in code, never called — an unknown door
|
|
31
|
+
"privileged": 3, # admin/internal path
|
|
32
|
+
"mixed_auth": 2, # seen with and without a credential
|
|
33
|
+
"no_auth_state": 2, # state-changing, no credential observed
|
|
34
|
+
"role_divergence": 2, # different roles, different answers
|
|
35
|
+
"only_errored": 1, # behaviour with valid input unseen
|
|
36
|
+
"credentialed": 1, # known surface, still worth confirming authz
|
|
37
|
+
"input_page": 2, # a server-rendered page that takes parameters
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _area_of(rg: ReasonGraph):
|
|
42
|
+
"""Map each endpoint label to its community (feature area) name + id."""
|
|
43
|
+
out = {}
|
|
44
|
+
for c in rg.communities():
|
|
45
|
+
for lbl in c["members"]:
|
|
46
|
+
out[lbl] = (c["id"], c["name"])
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _signals(n, roles: list[str]) -> list[dict]:
|
|
51
|
+
"""The structural questions this one endpoint raises, cited."""
|
|
52
|
+
a = n.attrs
|
|
53
|
+
ev = list(n.evidence[:3])
|
|
54
|
+
sig = []
|
|
55
|
+
path = a.get("path", "") or ""
|
|
56
|
+
priv = bool(PRIV_RE.search(path))
|
|
57
|
+
statuses = a.get("statuses") or []
|
|
58
|
+
reached = bool(a.get("requests"))
|
|
59
|
+
|
|
60
|
+
if a.get("api_state") == "STATIC_ONLY":
|
|
61
|
+
sig.append({"kind": "static_only", "weight": _WEIGHT["static_only"],
|
|
62
|
+
"observation": "named in client code but never requested in the capture",
|
|
63
|
+
"question": "what is this endpoint for, and which role is meant to call it?",
|
|
64
|
+
"next": "request it in an authorised session and read the response",
|
|
65
|
+
"evidence": ev})
|
|
66
|
+
if priv and reached:
|
|
67
|
+
sig.append({"kind": "privileged", "weight": _WEIGHT["privileged"],
|
|
68
|
+
"observation": "privileged-looking path that was actually reached"
|
|
69
|
+
+ (f" (roles: {', '.join(n.roles)})" if n.roles else ""),
|
|
70
|
+
"question": "was every caller supposed to see this, or is the boundary softer than intended?",
|
|
71
|
+
"next": "replay across two accounts and compare the responses",
|
|
72
|
+
"evidence": ev})
|
|
73
|
+
elif priv:
|
|
74
|
+
sig.append({"kind": "privileged", "weight": _WEIGHT["privileged"] - 1,
|
|
75
|
+
"observation": "privileged-looking path, only referenced in code",
|
|
76
|
+
"question": "who is allowed through this door?",
|
|
77
|
+
"next": "request it in an authorised session and observe who is admitted",
|
|
78
|
+
"evidence": ev})
|
|
79
|
+
if a.get("anonymous_requests") and a.get("credentials"):
|
|
80
|
+
sig.append({"kind": "mixed_auth", "weight": _WEIGHT["mixed_auth"],
|
|
81
|
+
"observation": "seen both with and without a credential",
|
|
82
|
+
"question": "is authentication actually required here, or only sometimes sent?",
|
|
83
|
+
"next": "replay it unauthenticated and compare",
|
|
84
|
+
"evidence": ev})
|
|
85
|
+
method = a.get("method")
|
|
86
|
+
if (reached and method not in (None, "*", "GET", "HEAD")
|
|
87
|
+
and not a.get("credentials")):
|
|
88
|
+
sig.append({"kind": "no_auth_state", "weight": _WEIGHT["no_auth_state"],
|
|
89
|
+
"observation": f"{method} with no credential observed on it",
|
|
90
|
+
"question": "does this state-changing call require identity?",
|
|
91
|
+
"next": "confirm whether it is meant to be authenticated",
|
|
92
|
+
"evidence": ev})
|
|
93
|
+
if len(roles) >= 2 and n.roles:
|
|
94
|
+
sbr = a.get("status_by_role", {})
|
|
95
|
+
got_2xx = [r for r in roles if any(200 <= s < 300 for s in sbr.get(r, []))]
|
|
96
|
+
got_deny = [r for r in roles if any(s in (401, 403) for s in sbr.get(r, []))]
|
|
97
|
+
if got_2xx and got_deny:
|
|
98
|
+
sig.append({"kind": "role_divergence", "weight": _WEIGHT["role_divergence"],
|
|
99
|
+
"observation": f"{', '.join(got_2xx)} got through; {', '.join(got_deny)} were denied",
|
|
100
|
+
"question": "is the difference an authorization control, or content that leaks anyway?",
|
|
101
|
+
"next": "diff the two responses for role-specific data",
|
|
102
|
+
"evidence": ev})
|
|
103
|
+
if statuses and all(s >= 400 for s in statuses):
|
|
104
|
+
sig.append({"kind": "only_errored", "weight": _WEIGHT["only_errored"],
|
|
105
|
+
"observation": f"only ever returned {', '.join(map(str, statuses))}",
|
|
106
|
+
"question": "how does it behave with valid input and auth?",
|
|
107
|
+
"next": "exercise the feature normally in an authorised session",
|
|
108
|
+
"evidence": ev})
|
|
109
|
+
return sig
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def investigation_plan(m: Model) -> dict:
|
|
113
|
+
"""A grounded scaffold for a target-specific methodology. Deterministic."""
|
|
114
|
+
rg = ReasonGraph(m)
|
|
115
|
+
roles = sorted(m.roles)
|
|
116
|
+
area_of = _area_of(rg)
|
|
117
|
+
eps = [n for n in m.nodes.values() if n.type == "endpoint"]
|
|
118
|
+
|
|
119
|
+
# group endpoints and their signals by feature area
|
|
120
|
+
areas: dict[str, dict] = {}
|
|
121
|
+
for n in eps:
|
|
122
|
+
aid, aname = area_of.get(n.label, ("cluster_?", "app"))
|
|
123
|
+
area = areas.setdefault(aid, {"id": aid, "name": aname, "endpoints": [],
|
|
124
|
+
"signals": [], "reached_by": set(), "score": 0})
|
|
125
|
+
area["endpoints"].append(n.label)
|
|
126
|
+
area["reached_by"].update(n.roles)
|
|
127
|
+
for s in _signals(n, roles):
|
|
128
|
+
area["signals"].append({**s, "endpoint": n.label})
|
|
129
|
+
area["score"] += s["weight"]
|
|
130
|
+
|
|
131
|
+
# Server-rendered apps put their input handling in pages, not API endpoints
|
|
132
|
+
# (DVWA's /vulnerabilities/sqli/?id=…). A page that takes parameters is a line of inquiry too.
|
|
133
|
+
pages = 0
|
|
134
|
+
for n in m.nodes.values():
|
|
135
|
+
if n.type != "route":
|
|
136
|
+
continue
|
|
137
|
+
params = sorted({m.nodes[dst].label for dst, e in rg.out.get(n.id, [])
|
|
138
|
+
if e.type == "USES_PARAMETER" and dst in m.nodes})
|
|
139
|
+
if not params:
|
|
140
|
+
continue
|
|
141
|
+
pages += 1
|
|
142
|
+
aid, aname = area_of.get(n.label, ("cluster_?", "app"))
|
|
143
|
+
area = areas.setdefault(aid, {"id": aid, "name": aname, "endpoints": [],
|
|
144
|
+
"signals": [], "reached_by": set(), "score": 0})
|
|
145
|
+
area["endpoints"].append(n.label)
|
|
146
|
+
area["reached_by"].update(n.roles)
|
|
147
|
+
sig = {"kind": "input_page", "weight": _WEIGHT["input_page"],
|
|
148
|
+
"observation": f"page takes input: {', '.join(params[:8])}",
|
|
149
|
+
"question": "how is each input handled — validated, reflected, stored, or passed to another system?",
|
|
150
|
+
"next": "vary one input at a time in an authorised session and compare the responses",
|
|
151
|
+
"evidence": list(n.evidence[:3]), "endpoint": n.label}
|
|
152
|
+
area["signals"].append(sig)
|
|
153
|
+
area["score"] += sig["weight"]
|
|
154
|
+
|
|
155
|
+
# attach the model's named unknowns to the area they concern
|
|
156
|
+
for u in m.unknowns:
|
|
157
|
+
ent = m.nodes[u.entity].label if u.entity in m.nodes else None
|
|
158
|
+
aid = area_of.get(ent, (None, None))[0] if ent else None
|
|
159
|
+
target = areas.get(aid)
|
|
160
|
+
if target is not None:
|
|
161
|
+
oq = target.setdefault("open_questions", [])
|
|
162
|
+
key = (u.type, ent)
|
|
163
|
+
if key not in {(q["type"], q["entity"]) for q in oq}:
|
|
164
|
+
oq.append({"type": u.type, "entity": ent,
|
|
165
|
+
"unknown": u.we_dont_know, "to_find_out": u.next_step})
|
|
166
|
+
|
|
167
|
+
lines = []
|
|
168
|
+
for area in sorted(areas.values(), key=lambda a: (-a["score"], a["name"])):
|
|
169
|
+
area["signals"].sort(key=lambda s: (-s["weight"], s["endpoint"]))
|
|
170
|
+
lines.append({
|
|
171
|
+
"area": area["name"], "id": area["id"], "leverage": area["score"],
|
|
172
|
+
"why_it_matters": _why(area),
|
|
173
|
+
"endpoints": sorted(set(area["endpoints"]))[:20],
|
|
174
|
+
"reached_by": sorted(area["reached_by"]),
|
|
175
|
+
"reach": _reach_summary(rg, area["endpoints"]),
|
|
176
|
+
"signals": area["signals"][:12],
|
|
177
|
+
"open_questions": area.get("open_questions", [])[:6],
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
# recon phase: what to establish before hunting, from the model's blind spots
|
|
181
|
+
recon = _recon(m, rg, roles)
|
|
182
|
+
coup = rg.coupling()
|
|
183
|
+
return {
|
|
184
|
+
"app": m.name,
|
|
185
|
+
"scope": m.scope,
|
|
186
|
+
"posture": {
|
|
187
|
+
"endpoints": len(eps),
|
|
188
|
+
"input_pages": pages,
|
|
189
|
+
"feature_areas": len([l for l in lines if l["endpoints"]]),
|
|
190
|
+
"roles": roles,
|
|
191
|
+
"static_only": sum(1 for n in eps if n.attrs.get("api_state") == "STATIC_ONLY"),
|
|
192
|
+
"privileged": sum(1 for n in eps if PRIV_RE.search(n.attrs.get("path", "") or "")),
|
|
193
|
+
"credentialed": sum(1 for n in eps if n.attrs.get("credentials")),
|
|
194
|
+
"open_questions": len(m.unknowns),
|
|
195
|
+
"third_parties": len(coup["to_third_party"]),
|
|
196
|
+
},
|
|
197
|
+
"recon": recon,
|
|
198
|
+
"lines_of_inquiry": lines,
|
|
199
|
+
"watch": {
|
|
200
|
+
"third_party_trust": coup["to_third_party"][:12],
|
|
201
|
+
"cross_area_coupling": coup["cross_community"][:12],
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _why(area: dict) -> str:
|
|
207
|
+
kinds = {s["kind"] for s in area["signals"]}
|
|
208
|
+
bits = []
|
|
209
|
+
if "static_only" in kinds:
|
|
210
|
+
bits.append("holds paths named in code but never walked")
|
|
211
|
+
if "privileged" in kinds:
|
|
212
|
+
bits.append("contains privileged-looking endpoints")
|
|
213
|
+
if "role_divergence" in kinds:
|
|
214
|
+
bits.append("roles diverge here")
|
|
215
|
+
if "mixed_auth" in kinds or "no_auth_state" in kinds:
|
|
216
|
+
bits.append("the auth boundary is ambiguous")
|
|
217
|
+
if not bits:
|
|
218
|
+
bits.append("a mapped part of the surface to confirm")
|
|
219
|
+
return "; ".join(bits) + "."
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _reach_summary(rg: ReasonGraph, ep_labels: list[str]) -> list[str]:
|
|
223
|
+
"""A few 'how this area is reached' notes, distinct entry points."""
|
|
224
|
+
entries: dict[str, str] = {}
|
|
225
|
+
id_by_label = {n.label: nid for nid, n in rg.m.nodes.items() if n.type == "endpoint"}
|
|
226
|
+
for lbl in ep_labels[:8]:
|
|
227
|
+
nid = id_by_label.get(lbl)
|
|
228
|
+
if not nid:
|
|
229
|
+
continue
|
|
230
|
+
for p in rg.reach(nid).get("paths", [])[:2]:
|
|
231
|
+
if p["hops"]:
|
|
232
|
+
state = "code" if any(h["state"] == "INFERRED" for h in p["hops"]) else "traffic"
|
|
233
|
+
entries.setdefault(p["entry"], state)
|
|
234
|
+
return [f"from {e} (via {state})" for e, state in sorted(entries.items())][:5]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _recon(m: Model, rg: ReasonGraph, roles: list[str]) -> list[dict]:
|
|
238
|
+
"""What to establish first — from the capture's own blind spots."""
|
|
239
|
+
out = []
|
|
240
|
+
types = {u.type for u in m.unknowns}
|
|
241
|
+
if "AUTHENTICATED_STATE_NOT_OBSERVED" in types:
|
|
242
|
+
out.append({"step": "get an authenticated capture",
|
|
243
|
+
"why": "no credentialed traffic was seen, so the app's real surface is unmapped",
|
|
244
|
+
"how": "log in and re-capture, building with --role <name>"})
|
|
245
|
+
if not roles or "ROLE_NOT_TAGGED" in types:
|
|
246
|
+
out.append({"step": "capture each role separately",
|
|
247
|
+
"why": "cross-role comparison needs traffic tagged by who made it",
|
|
248
|
+
"how": "build each account's capture with --role, then `cross-role`"})
|
|
249
|
+
static = [n for n in m.nodes.values()
|
|
250
|
+
if n.type == "endpoint" and n.attrs.get("api_state") == "STATIC_ONLY"]
|
|
251
|
+
if static:
|
|
252
|
+
out.append({"step": "walk the paths found only in code",
|
|
253
|
+
"why": f"{len(static)} endpoint(s) are referenced in scripts but were never called — the biggest map gaps",
|
|
254
|
+
"how": "request each in an authorised session and observe"})
|
|
255
|
+
if rg.coupling()["to_third_party"]:
|
|
256
|
+
out.append({"step": "account for third-party code",
|
|
257
|
+
"why": "the app runs code it does not control; understand what those origins receive",
|
|
258
|
+
"how": "review the Supply chain view and what each third party is sent"})
|
|
259
|
+
if not out:
|
|
260
|
+
out.append({"step": "confirm the map is complete",
|
|
261
|
+
"why": "the capture looks well-covered; verify no feature area is missing",
|
|
262
|
+
"how": "compare the endpoint list against the app's visible navigation"})
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
METHODOLOGY_PROMPT = [
|
|
267
|
+
"You are a senior application-security researcher planning how to investigate ONE web application you are authorised to test. Below is an evidence-backed model of it, built from captured traffic, plus a grounded scaffold: the app's feature areas, how each is reached, its trust posture, and what the capture could not answer. Every item cites evidence ids (ev_N) that resolve to real requests.",
|
|
268
|
+
"",
|
|
269
|
+
"Produce a RECON AND HUNTING METHODOLOGY specific to THIS application. Not a generic checklist — a plan that reads as if written after studying this app's structure.",
|
|
270
|
+
"",
|
|
271
|
+
"Rules that make it a methodology and not a list:",
|
|
272
|
+
"- Sequence it. Recon first (close the blind spots the scaffold names), then mapping, then focused investigation per feature area, ordered by where understanding pays off most (use the `leverage` and `why_it_matters` the scaffold gives, and your own reading of the graph).",
|
|
273
|
+
"- Ground every step in this model. Reference the actual endpoints, areas, roles, execution flows and unknowns by name, and cite the ev_N behind each. Never introduce an endpoint, parameter or edge the model does not contain.",
|
|
274
|
+
"- For each line of inquiry, state: what the evidence shows, what is unknown, the specific question worth answering, and the single next observation or two-account comparison that would answer it. Prefer what the graph supports over any OWASP category.",
|
|
275
|
+
"- Explain the reasoning, not just the action: why this area, why this order, what an answer here tells you about the rest of the app.",
|
|
276
|
+
"- Everything is a hypothesis to verify safely in an authorised session. No payloads, no exploits — the goal is to understand the app deeply and know where to look, faster and better than a checklist would.",
|
|
277
|
+
"",
|
|
278
|
+
"Structure your answer as: (1) What this app is, in three sentences from the model. (2) Recon — what to establish first and why. (3) A prioritised set of lines of inquiry, one per feature area, each as described above. (4) What stays uncertain and the capture that would resolve it.",
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# How much resolving each kind of unknown improves the picture. Higher = the
|
|
283
|
+
# answer teaches you more about the app, so it belongs at the top of the queue.
|
|
284
|
+
_GAP_WEIGHT = {
|
|
285
|
+
"AUTHENTICATED_STATE_NOT_OBSERVED": 5, # a whole side of the app is unseen
|
|
286
|
+
"API_PURPOSE_UNKNOWN": 4, # a code-only path — what is it?
|
|
287
|
+
"ROLE_NOT_TAGGED": 4,
|
|
288
|
+
"AUTHORIZATION_UNKNOWN": 3,
|
|
289
|
+
"ENDPOINT_ONLY_ERRORED": 2,
|
|
290
|
+
"SCRIPT_PARTIALLY_SCANNED": 2,
|
|
291
|
+
"CAPTURE_ITEMS_SKIPPED": 1,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def rank_gaps(m: Model) -> dict:
|
|
296
|
+
"""Rank the model's unknowns by how much resolving each improves the picture,
|
|
297
|
+
each with the smallest next step. Good recon says what you have not seen yet."""
|
|
298
|
+
priv_paths = {n.label for n in m.nodes.values()
|
|
299
|
+
if n.type == "endpoint" and PRIV_RE.search(n.attrs.get("path", "") or "")}
|
|
300
|
+
ranked = []
|
|
301
|
+
for u in m.unknowns:
|
|
302
|
+
ent = m.nodes[u.entity].label if u.entity in m.nodes else u.entity.split(":")[-1]
|
|
303
|
+
w = _GAP_WEIGHT.get(u.type, 1)
|
|
304
|
+
if ent in priv_paths:
|
|
305
|
+
w += 1 # an unknown on a privileged path is worth more
|
|
306
|
+
ranked.append({
|
|
307
|
+
"leverage": w, "type": u.type, "entity": ent,
|
|
308
|
+
"we_know": u.we_know, "unknown": u.we_dont_know,
|
|
309
|
+
"next": u.next_step,
|
|
310
|
+
})
|
|
311
|
+
ranked.sort(key=lambda g: (-g["leverage"], g["type"], g["entity"]))
|
|
312
|
+
return {
|
|
313
|
+
"app": m.name,
|
|
314
|
+
"note": "Ranked by how much resolving each improves the picture. "
|
|
315
|
+
"Absence is a gap in observation, never a statement about the target.",
|
|
316
|
+
"gaps": ranked,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
FALSIFY_PROMPT = [
|
|
321
|
+
"You are a senior application-security researcher practising disciplined doubt. Below is an evidence-backed model of one web application and a set of hypotheses drawn from its structure (the investigation scaffold). Your job is the OPPOSITE of proving something is vulnerable: for each hypothesis, try to ELIMINATE it from the evidence alone.",
|
|
322
|
+
"",
|
|
323
|
+
"For each hypothesis:",
|
|
324
|
+
"- State what would have to be true for it to hold, and whether the model already contradicts it (cite ev_N).",
|
|
325
|
+
"- If the evidence neither confirms nor kills it, say exactly what single observation would falsify it — the cheapest test that could prove it wrong.",
|
|
326
|
+
"- Prefer to discard weak hypotheses. A hypothesis that survives honest attempts to kill it is the only kind worth a researcher's time.",
|
|
327
|
+
"",
|
|
328
|
+
"Rules: reason only from the model; never invent an endpoint, parameter or edge; every claim cites ev_N; produce questions and falsifying tests, never payloads or exploits. Rank what SURVIVES scrutiny, and say plainly what you were able to rule out and why.",
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def methodology_package(m: Model, cap: int = 60, prompt=None) -> dict:
|
|
333
|
+
"""Everything an AI needs to write a target-specific methodology (or, with
|
|
334
|
+
a different `prompt`, to falsify the scaffold's hypotheses)."""
|
|
335
|
+
from .context import context_package
|
|
336
|
+
ctx = context_package(m, lens="attention", cap=cap, graph=True)
|
|
337
|
+
return {
|
|
338
|
+
"instructions": prompt or METHODOLOGY_PROMPT,
|
|
339
|
+
"plan_scaffold": investigation_plan(m),
|
|
340
|
+
"model": {k: ctx[k] for k in ("app", "scope", "roles", "shape", "graph",
|
|
341
|
+
"endpoints", "unknowns", "evidence")
|
|
342
|
+
if k in ctx},
|
|
343
|
+
}
|