proxy2model 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. proxy2model/__init__.py +19 -0
  2. proxy2model/__main__.py +6 -0
  3. proxy2model/atomic.py +68 -0
  4. proxy2model/cli/__init__.py +3 -0
  5. proxy2model/cli/analyze.py +501 -0
  6. proxy2model/cli/app.py +408 -0
  7. proxy2model/cli/browse.py +279 -0
  8. proxy2model/cli/build.py +399 -0
  9. proxy2model/cli/common.py +190 -0
  10. proxy2model/cli/history.py +224 -0
  11. proxy2model/cli/mcp.py +184 -0
  12. proxy2model/cli/ui.py +62 -0
  13. proxy2model/core/__init__.py +1 -0
  14. proxy2model/core/changes.py +124 -0
  15. proxy2model/core/context.py +571 -0
  16. proxy2model/core/diff.py +330 -0
  17. proxy2model/core/domains.py +98 -0
  18. proxy2model/core/graph.py +562 -0
  19. proxy2model/core/methodology.py +343 -0
  20. proxy2model/core/model.py +998 -0
  21. proxy2model/core/stack.py +218 -0
  22. proxy2model/data/fonts/LICENSES.md +16 -0
  23. proxy2model/data/fonts/inter-latin-wght-normal.woff2 +0 -0
  24. proxy2model/data/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  25. proxy2model/data/fonts/nippo-200.woff2 +0 -0
  26. proxy2model/data/fonts/nippo-300.woff2 +0 -0
  27. proxy2model/data/fonts/nippo-400.woff2 +0 -0
  28. proxy2model/data/fonts/nippo-500.woff2 +0 -0
  29. proxy2model/data/fonts/nippo-700.woff2 +0 -0
  30. proxy2model/data/public_suffix_list.dat +16497 -0
  31. proxy2model/db/__init__.py +1 -0
  32. proxy2model/db/bql.py +710 -0
  33. proxy2model/db/bql_js.py +278 -0
  34. proxy2model/db/store.py +547 -0
  35. proxy2model/export/__init__.py +1 -0
  36. proxy2model/export/openapi.py +248 -0
  37. proxy2model/ingest/__init__.py +1 -0
  38. proxy2model/ingest/browse.py +181 -0
  39. proxy2model/ingest/browser.py +1058 -0
  40. proxy2model/ingest/crawl.py +673 -0
  41. proxy2model/ingest/extract.py +208 -0
  42. proxy2model/ingest/infer.py +75 -0
  43. proxy2model/ingest/journey.py +148 -0
  44. proxy2model/ingest/live.py +96 -0
  45. proxy2model/ingest/parse.py +1275 -0
  46. proxy2model/ingest/redact.py +535 -0
  47. proxy2model/py.typed +0 -0
  48. proxy2model/recon/__init__.py +1 -0
  49. proxy2model/recon/osint.py +607 -0
  50. proxy2model/report/__init__.py +1 -0
  51. proxy2model/report/animate.py +163 -0
  52. proxy2model/report/dash.py +469 -0
  53. proxy2model/report/fonts.py +51 -0
  54. proxy2model/report/html.py +988 -0
  55. proxy2model/report/inventory.py +258 -0
  56. proxy2model/report/map.py +515 -0
  57. proxy2model/report/pages.py +537 -0
  58. proxy2model/report/query.py +306 -0
  59. proxy2model/report/shot.py +55 -0
  60. proxy2model/report/view.py +213 -0
  61. proxy2model-2.0.0.dist-info/METADATA +148 -0
  62. proxy2model-2.0.0.dist-info/RECORD +66 -0
  63. proxy2model-2.0.0.dist-info/WHEEL +5 -0
  64. proxy2model-2.0.0.dist-info/entry_points.txt +2 -0
  65. proxy2model-2.0.0.dist-info/licenses/LICENSE +201 -0
  66. proxy2model-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,124 @@
1
+ """
2
+ Diff two models of the same app, grouped into feature areas.
3
+
4
+ Recon is not a one-shot: an app changes, and the interesting question at each
5
+ release is *what moved*. This compares an older model to the current one and
6
+ reports the drift — what appeared, what is gone — grouped by feature area and
7
+ annotated with why a researcher would care.
8
+
9
+ It never says "this is a bug". A new admin endpoint or a new third party is a
10
+ change in the attack surface worth a look, stated as such and cited to the
11
+ evidence in the current model.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from .graph import ReasonGraph
17
+ from .model import PRIV_RE, Model, match_key
18
+
19
+
20
+ def _by_type(m: Model, t: str) -> dict[str, object]:
21
+ return {nid: n for nid, n in m.nodes.items() if n.type == t}
22
+
23
+
24
+ def _why_appeared(kind: str, node) -> str | None:
25
+ path = node.attrs.get("path", "") or ""
26
+ if kind == "endpoint":
27
+ if PRIV_RE.search(path):
28
+ return "a new privileged-looking endpoint — confirm who may reach it"
29
+ if node.attrs.get("api_state") == "STATIC_ONLY":
30
+ return "new, and only referenced in code — an unwalked path to try"
31
+ return "a new endpoint in the surface"
32
+ if kind == "third_party":
33
+ return "new third-party code the app now talks to — trust left the app"
34
+ if kind == "host":
35
+ return "a new first-party host in scope"
36
+ if kind == "tech":
37
+ return "a newly advertised technology marker"
38
+ if kind == "cookie":
39
+ return "a new cookie — check its flags and what it carries"
40
+ if kind == "auth":
41
+ return "a new credential scheme on the surface"
42
+ return None
43
+
44
+
45
+ def _why_gone(kind: str) -> str | None:
46
+ if kind == "endpoint":
47
+ return "an endpoint no longer seen — retired, renamed, or just not walked this time"
48
+ if kind == "third_party":
49
+ return "a third party the app no longer talks to"
50
+ if kind == "host":
51
+ return "a host no longer in the capture"
52
+ return None
53
+
54
+
55
+ def diff_models(old: Model, new: Model) -> dict:
56
+ """What moved between two models of the same app. Drift, never findings."""
57
+ rg = ReasonGraph(new)
58
+ area_of = {}
59
+ for c in rg.communities():
60
+ for lbl in c["members"]:
61
+ area_of[lbl] = c["name"]
62
+
63
+ KINDS = ("endpoint", "route", "script", "host", "third_party", "cookie", "auth", "tech")
64
+ areas: dict[str, dict] = {}
65
+ totals = {"appeared": 0, "gone": 0}
66
+
67
+ def area(name):
68
+ return areas.setdefault(name, {"area": name, "appeared": [], "gone": []})
69
+
70
+ def _keys(m: Model, method_known: bool) -> set:
71
+ return {(n.attrs.get("host"), match_key(n.attrs.get("path", "")))
72
+ for n in _by_type(m, "endpoint").values()
73
+ if bool(n.attrs.get("method_known", True)) == method_known}
74
+
75
+ # a path named in code with no method (`* /api/x`) that a later capture
76
+ # actually walked (`GET /api/x`) was not retired — it was resolved
77
+ old_unwalked = _keys(old, False)
78
+ now_walked = _keys(new, True)
79
+
80
+ for kind in KINDS:
81
+ old_ids = set(_by_type(old, kind))
82
+ new_nodes = _by_type(new, kind)
83
+ new_ids = set(new_nodes)
84
+ old_nodes = _by_type(old, kind)
85
+ for nid in sorted(new_ids - old_ids):
86
+ n = new_nodes[nid]
87
+ why = _why_appeared(kind, n)
88
+ if kind == "endpoint" and (n.attrs.get("host"), match_key(n.attrs.get("path", ""))) \
89
+ in old_unwalked and n.attrs.get("method_known", True):
90
+ why = "named in code before, now walked in traffic — a gap closed"
91
+ if why is None:
92
+ continue
93
+ area(area_of.get(n.label, kind)) ["appeared"].append({
94
+ "kind": kind, "label": n.label, "why": why,
95
+ "state": n.attrs.get("api_state"),
96
+ "evidence": list(n.evidence[:3])})
97
+ totals["appeared"] += 1
98
+ for nid in sorted(old_ids - new_ids):
99
+ n = old_nodes[nid]
100
+ why = _why_gone(kind)
101
+ if kind == "endpoint" and not n.attrs.get("method_known", True) and \
102
+ (n.attrs.get("host"), match_key(n.attrs.get("path", ""))) in now_walked:
103
+ continue # resolved by traffic, reported above as a closed gap
104
+ if why is None:
105
+ continue
106
+ area(area_of.get(n.label, kind))["gone"].append({
107
+ "kind": kind, "label": n.label, "why": why})
108
+ totals["gone"] += 1
109
+
110
+ lines = []
111
+ for a in sorted(areas.values(),
112
+ key=lambda a: (-(len(a["appeared"]) + len(a["gone"])), a["area"])):
113
+ if a["appeared"] or a["gone"]:
114
+ a["appeared"].sort(key=lambda x: (x["kind"], x["label"]))
115
+ a["gone"].sort(key=lambda x: (x["kind"], x["label"]))
116
+ lines.append(a)
117
+
118
+ return {
119
+ "app": new.name,
120
+ "totals": totals,
121
+ "note": "Drift between two captures. An appearance is a change in the "
122
+ "attack surface to understand, never a finding.",
123
+ "areas": lines,
124
+ }