pypproxy 0.1.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 (72) hide show
  1. pypproxy/__init__.py +0 -0
  2. pypproxy/api/__init__.py +0 -0
  3. pypproxy/api/server.py +427 -0
  4. pypproxy/bulk/__init__.py +0 -0
  5. pypproxy/bulk/sender.py +97 -0
  6. pypproxy/cert/__init__.py +0 -0
  7. pypproxy/cert/ca.py +144 -0
  8. pypproxy/cert/client_cert.py +65 -0
  9. pypproxy/codec.py +176 -0
  10. pypproxy/config/__init__.py +0 -0
  11. pypproxy/config/config.py +106 -0
  12. pypproxy/dns/__init__.py +0 -0
  13. pypproxy/dns/server.py +149 -0
  14. pypproxy/exporter/__init__.py +0 -0
  15. pypproxy/exporter/exporter.py +122 -0
  16. pypproxy/exporter/importer.py +169 -0
  17. pypproxy/graphql/__init__.py +0 -0
  18. pypproxy/graphql/detector.py +76 -0
  19. pypproxy/graphql/introspection.py +217 -0
  20. pypproxy/graphql/modifier.py +98 -0
  21. pypproxy/graphql/schema_store.py +33 -0
  22. pypproxy/intercept/__init__.py +0 -0
  23. pypproxy/intercept/manager.py +142 -0
  24. pypproxy/interceptor/__init__.py +0 -0
  25. pypproxy/interceptor/interceptor.py +172 -0
  26. pypproxy/proto/__init__.py +0 -0
  27. pypproxy/proto/grpc.py +48 -0
  28. pypproxy/proto/mqtt.py +119 -0
  29. pypproxy/proto/ws.py +120 -0
  30. pypproxy/proto/ws_intercept.py +117 -0
  31. pypproxy/proxy/__init__.py +0 -0
  32. pypproxy/proxy/proxy.py +407 -0
  33. pypproxy/replay/__init__.py +0 -0
  34. pypproxy/replay/replay.py +77 -0
  35. pypproxy/rule/__init__.py +0 -0
  36. pypproxy/rule/rule.py +198 -0
  37. pypproxy/scan/__init__.py +0 -0
  38. pypproxy/scan/scanner.py +296 -0
  39. pypproxy/script/__init__.py +0 -0
  40. pypproxy/script/engine.py +49 -0
  41. pypproxy/security/__init__.py +0 -0
  42. pypproxy/security/header_checker.py +308 -0
  43. pypproxy/security/int_overflow.py +193 -0
  44. pypproxy/security/jwt_checker.py +273 -0
  45. pypproxy/security/plugin.py +152 -0
  46. pypproxy/security/randomness.py +165 -0
  47. pypproxy/store/__init__.py +0 -0
  48. pypproxy/store/db.py +189 -0
  49. pypproxy/store/filter_parser.py +181 -0
  50. pypproxy/store/fts.py +105 -0
  51. pypproxy/store/models.py +81 -0
  52. pypproxy/store/scope.py +63 -0
  53. pypproxy/store/store.py +120 -0
  54. pypproxy/ui/__init__.py +0 -0
  55. pypproxy/ui/app.py +386 -0
  56. pypproxy/ui/bulk_sender_ui.py +125 -0
  57. pypproxy/ui/cui.py +162 -0
  58. pypproxy/ui/detail.py +179 -0
  59. pypproxy/ui/diff_view.py +118 -0
  60. pypproxy/ui/graphql_tab.py +265 -0
  61. pypproxy/ui/import_tab.py +136 -0
  62. pypproxy/ui/intercept_dialog.py +74 -0
  63. pypproxy/ui/resender.py +140 -0
  64. pypproxy/ui/scan_tab.py +98 -0
  65. pypproxy/ui/security_tab.py +356 -0
  66. pypproxy/ui/settings.py +413 -0
  67. pypproxy/ui/theme.py +59 -0
  68. pypproxy-0.1.0.dist-info/METADATA +19 -0
  69. pypproxy-0.1.0.dist-info/RECORD +72 -0
  70. pypproxy-0.1.0.dist-info/WHEEL +4 -0
  71. pypproxy-0.1.0.dist-info/entry_points.txt +2 -0
  72. pypproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from nicegui import ui
4
+
5
+ from pypproxy.store.models import Entry
6
+ from pypproxy.store.store import Store
7
+
8
+
9
+ def build_scan_tab(store: Store) -> dict:
10
+ """Build the Active Scan tab. Returns state with open_entry method."""
11
+ state: dict = {"entry": None}
12
+
13
+ with ui.column().classes("w-full h-full overflow-auto q-pa-md"):
14
+ entry_label = ui.label("No entry selected").classes("text-grey text-caption q-mb-sm")
15
+
16
+ with ui.row().classes("items-center gap-4 q-mb-md"):
17
+ cats = (
18
+ ui.select(
19
+ ["xss", "sqli", "cmdi", "ssti", "path_traversal"],
20
+ multiple=True,
21
+ value=["xss", "sqli"],
22
+ label="Categories",
23
+ )
24
+ .props("dense outlined dark")
25
+ .classes("w-72")
26
+ )
27
+ conc = (
28
+ ui.number(label="Concurrency", value=5, min=1, max=20)
29
+ .props("dense outlined dark")
30
+ .classes("w-24")
31
+ )
32
+ run_btn = ui.button("Scan", icon="search").props("color=negative")
33
+
34
+ summary_label = ui.label("").classes("text-caption text-grey q-mb-xs")
35
+
36
+ results_table = (
37
+ ui.table(
38
+ columns=[
39
+ {"name": "param", "label": "Param", "field": "param", "align": "left"},
40
+ {"name": "cat", "label": "Category", "field": "category", "align": "center"},
41
+ {"name": "payload", "label": "Payload", "field": "payload", "align": "left"},
42
+ {
43
+ "name": "status",
44
+ "label": "Status",
45
+ "field": "status_code",
46
+ "align": "center",
47
+ },
48
+ {"name": "ms", "label": "ms", "field": "duration_ms", "align": "right"},
49
+ {"name": "suspicious", "label": "⚠", "field": "suspicious", "align": "center"},
50
+ {"name": "reason", "label": "Reason", "field": "reason", "align": "left"},
51
+ ],
52
+ rows=[],
53
+ row_key="payload",
54
+ )
55
+ .classes("w-full")
56
+ .props("dense flat dark")
57
+ )
58
+ results_table.add_slot(
59
+ "body-cell-suspicious",
60
+ """
61
+ <q-td :props="props">
62
+ <q-badge v-if="props.value" color="negative" label="!" />
63
+ </q-td>
64
+ """,
65
+ )
66
+
67
+ async def _scan() -> None:
68
+ from pypproxy.scan.scanner import run_scan
69
+
70
+ entry = state.get("entry")
71
+ if not entry:
72
+ ui.notify("No entry selected", type="warning")
73
+ return
74
+ run_btn.props("loading")
75
+ try:
76
+ results = await run_scan(
77
+ entry,
78
+ categories=list(cats.value) if cats.value else None,
79
+ concurrency=int(conc.value or 5),
80
+ )
81
+ results_table.rows = [r.to_dict() for r in results]
82
+ results_table.update()
83
+ suspicious = sum(1 for r in results if r.suspicious)
84
+ summary_label.text = f"{len(results)} tests — {suspicious} findings"
85
+ if suspicious:
86
+ ui.notify(f"⚠ {suspicious} potential findings!", type="warning")
87
+ else:
88
+ ui.notify(f"Scan complete — {len(results)} tests, no findings", type="positive")
89
+ finally:
90
+ run_btn.props(remove="loading")
91
+
92
+ run_btn.on("click", _scan)
93
+
94
+ def open_entry(entry: Entry) -> None:
95
+ state["entry"] = entry
96
+ entry_label.text = f"#{entry.id} {entry.method} {entry.scheme}://{entry.host}{entry.path}"
97
+
98
+ return {"open_entry": open_entry}
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ from nicegui import ui
4
+
5
+ from pypproxy.security.header_checker import check_security_headers
6
+ from pypproxy.security.jwt_checker import extract_jwt_from_headers
7
+ from pypproxy.security.jwt_checker import run_checks as jwt_run_checks
8
+ from pypproxy.security.randomness import analyse_token
9
+ from pypproxy.store.models import Entry
10
+ from pypproxy.store.store import Store
11
+
12
+
13
+ def build_security_tab(store: Store) -> dict:
14
+ """Build the security tools tab. Returns state with open_entry method."""
15
+ state: dict = {"entry": None}
16
+
17
+ with ui.column().classes("w-full h-full overflow-auto q-pa-md"):
18
+ entry_label = ui.label(
19
+ "No entry selected — right-click a traffic row → Security check"
20
+ ).classes("text-grey text-caption q-mb-sm")
21
+
22
+ with ui.tabs().props("dense dark") as sec_tabs:
23
+ jwt_tab = ui.tab("JWT Checker", icon="key")
24
+ header_tab = ui.tab("Security Headers", icon="shield")
25
+ randomness_tab = ui.tab("Token Randomness", icon="casino")
26
+ overflow_tab = ui.tab("Int Overflow", icon="numbers")
27
+
28
+ with ui.tab_panels(sec_tabs, value=jwt_tab).classes("w-full"):
29
+ # --- JWT Checker ---
30
+ with ui.tab_panel(jwt_tab):
31
+ ui.label("JWT Vulnerability Checker").classes("text-subtitle2 q-mb-xs")
32
+ ui.label(
33
+ "Automatically extracts the JWT from the Authorization header and tests common attack vectors."
34
+ ).classes("text-caption text-grey q-mb-md")
35
+
36
+ token_input = (
37
+ ui.input(label="JWT Token (auto-filled from entry)")
38
+ .props("outlined dense dark")
39
+ .classes("w-full font-mono text-xs q-mb-sm")
40
+ )
41
+ jwt_run_btn = ui.button("Run JWT Checks", icon="play_arrow").props("color=primary")
42
+ jwt_results_label = ui.label("").classes("text-caption text-grey q-my-xs")
43
+ jwt_table = (
44
+ ui.table(
45
+ columns=[
46
+ {
47
+ "name": "vector",
48
+ "label": "Vector",
49
+ "field": "vector",
50
+ "align": "left",
51
+ },
52
+ {
53
+ "name": "status",
54
+ "label": "Status",
55
+ "field": "status_code",
56
+ "align": "center",
57
+ },
58
+ {"name": "ms", "label": "ms", "field": "duration_ms", "align": "right"},
59
+ {
60
+ "name": "suspicious",
61
+ "label": "Suspicious",
62
+ "field": "suspicious",
63
+ "align": "center",
64
+ },
65
+ {
66
+ "name": "desc",
67
+ "label": "Description",
68
+ "field": "description",
69
+ "align": "left",
70
+ },
71
+ ],
72
+ rows=[],
73
+ row_key="vector",
74
+ )
75
+ .classes("w-full")
76
+ .props("dense flat dark")
77
+ )
78
+ jwt_table.add_slot(
79
+ "body-cell-suspicious",
80
+ """
81
+ <q-td :props="props">
82
+ <q-badge v-if="props.value" color="negative" label="⚠ YES" />
83
+ <q-badge v-else color="grey" label="no" />
84
+ </q-td>
85
+ """,
86
+ )
87
+
88
+ async def _run_jwt() -> None:
89
+ entry = state.get("entry")
90
+ token = token_input.value.strip()
91
+ if not token or not entry:
92
+ ui.notify("No token or entry", type="warning")
93
+ return
94
+ jwt_run_btn.props("loading")
95
+ try:
96
+ results = await jwt_run_checks(
97
+ token=token,
98
+ entry_id=entry.id,
99
+ method=entry.method,
100
+ scheme=entry.scheme,
101
+ host=entry.host,
102
+ path=entry.path,
103
+ query=entry.query,
104
+ headers=entry.req_headers,
105
+ body=entry.req_body,
106
+ )
107
+ jwt_table.rows = [r.to_dict() for r in results]
108
+ jwt_table.update()
109
+ suspicious = sum(1 for r in results if r.suspicious)
110
+ jwt_results_label.text = (
111
+ f"{len(results)} vectors tested — {suspicious} suspicious"
112
+ )
113
+ if suspicious:
114
+ ui.notify(f"⚠ {suspicious} suspicious responses!", type="warning")
115
+ else:
116
+ ui.notify("Done — no obvious vulnerabilities detected", type="positive")
117
+ finally:
118
+ jwt_run_btn.props(remove="loading")
119
+
120
+ jwt_run_btn.on("click", _run_jwt)
121
+
122
+ # --- Security Headers ---
123
+ with ui.tab_panel(header_tab):
124
+ ui.label("Security Header Checker").classes("text-subtitle2 q-mb-xs")
125
+ ui.label(
126
+ "Checks the response headers of the selected entry for common security misconfigurations."
127
+ ).classes("text-caption text-grey q-mb-md")
128
+
129
+ hdr_run_btn = ui.button("Check Headers", icon="shield").props("color=primary")
130
+ hdr_results_label = ui.label("").classes("text-caption text-grey q-my-xs")
131
+ hdr_table = (
132
+ ui.table(
133
+ columns=[
134
+ {
135
+ "name": "header",
136
+ "label": "Header",
137
+ "field": "header",
138
+ "align": "left",
139
+ },
140
+ {
141
+ "name": "present",
142
+ "label": "Present",
143
+ "field": "present",
144
+ "align": "center",
145
+ },
146
+ {"name": "passed", "label": "OK", "field": "passed", "align": "center"},
147
+ {
148
+ "name": "severity",
149
+ "label": "Severity",
150
+ "field": "severity",
151
+ "align": "center",
152
+ },
153
+ {
154
+ "name": "detail",
155
+ "label": "Detail",
156
+ "field": "detail",
157
+ "align": "left",
158
+ },
159
+ ],
160
+ rows=[],
161
+ row_key="header",
162
+ )
163
+ .classes("w-full")
164
+ .props("dense flat dark")
165
+ )
166
+ hdr_table.add_slot(
167
+ "body-cell-passed",
168
+ """
169
+ <q-td :props="props">
170
+ <q-badge :color="props.value ? 'positive' : 'negative'" :label="props.value ? '✓' : '✗'" />
171
+ </q-td>
172
+ """,
173
+ )
174
+ hdr_table.add_slot(
175
+ "body-cell-severity",
176
+ """
177
+ <q-td :props="props">
178
+ <q-badge :color="{'high':'negative','medium':'warning','low':'orange','info':'grey'}[props.value] || 'grey'"
179
+ :label="props.value" />
180
+ </q-td>
181
+ """,
182
+ )
183
+
184
+ def _run_headers() -> None:
185
+ entry = state.get("entry")
186
+ if not entry or not entry.resp_headers:
187
+ ui.notify("No response headers in selected entry", type="warning")
188
+ return
189
+ results = check_security_headers(entry.resp_headers)
190
+ hdr_table.rows = [r.to_dict() for r in results]
191
+ hdr_table.update()
192
+ fails = sum(1 for r in results if not r.passed)
193
+ hdr_results_label.text = f"{len(results)} headers checked — {fails} issues"
194
+ if fails:
195
+ ui.notify(f"{fails} security header issues found", type="warning")
196
+ else:
197
+ ui.notify("All security headers look good", type="positive")
198
+
199
+ hdr_run_btn.on("click", _run_headers)
200
+
201
+ # --- Token Randomness ---
202
+ with ui.tab_panel(randomness_tab):
203
+ ui.label("Token Randomness Analyser").classes("text-subtitle2 q-mb-xs")
204
+ ui.label(
205
+ "Statistical entropy tests on tokens extracted from the selected entry."
206
+ ).classes("text-caption text-grey q-mb-md")
207
+
208
+ rand_token_input = (
209
+ ui.input(label="Token (auto-filled from entry)")
210
+ .props("outlined dense dark")
211
+ .classes("w-full font-mono text-xs q-mb-sm")
212
+ )
213
+ rand_run_btn = ui.button("Analyse", icon="casino").props("color=primary")
214
+ rand_table = (
215
+ ui.table(
216
+ columns=[
217
+ {
218
+ "name": "test",
219
+ "label": "Test",
220
+ "field": "test_name",
221
+ "align": "left",
222
+ },
223
+ {
224
+ "name": "passed",
225
+ "label": "Passed",
226
+ "field": "passed",
227
+ "align": "center",
228
+ },
229
+ {"name": "score", "label": "Score", "field": "score", "align": "right"},
230
+ {
231
+ "name": "detail",
232
+ "label": "Detail",
233
+ "field": "detail",
234
+ "align": "left",
235
+ },
236
+ ],
237
+ rows=[],
238
+ row_key="test_name",
239
+ )
240
+ .classes("w-full")
241
+ .props("dense flat dark")
242
+ )
243
+ rand_table.add_slot(
244
+ "body-cell-passed",
245
+ """
246
+ <q-td :props="props">
247
+ <q-badge :color="props.value ? 'positive' : 'negative'" :label="props.value ? '✓' : '✗'" />
248
+ </q-td>
249
+ """,
250
+ )
251
+
252
+ def _run_randomness() -> None:
253
+ token = rand_token_input.value.strip()
254
+ if not token:
255
+ ui.notify("Enter a token", type="warning")
256
+ return
257
+ results = analyse_token(token)
258
+ rand_table.rows = [r.to_dict() for r in results]
259
+ rand_table.update()
260
+ fails = sum(1 for r in results if not r.passed)
261
+ if fails:
262
+ ui.notify(
263
+ f"{fails} randomness tests failed — token may be weak", type="warning"
264
+ )
265
+ else:
266
+ ui.notify("Token appears sufficiently random", type="positive")
267
+
268
+ rand_run_btn.on("click", _run_randomness)
269
+
270
+ # --- Int Overflow ---
271
+ with ui.tab_panel(overflow_tab):
272
+ ui.label("Integer Overflow / Boundary Tester").classes("text-subtitle2 q-mb-xs")
273
+ ui.label(
274
+ "Finds integer parameters in the selected entry and tests boundary values."
275
+ ).classes("text-caption text-grey q-mb-md")
276
+
277
+ overflow_run_btn = ui.button("Run Tests", icon="numbers").props("color=primary")
278
+ overflow_label = ui.label("").classes("text-caption text-grey q-my-xs")
279
+ overflow_table = (
280
+ ui.table(
281
+ columns=[
282
+ {"name": "param", "label": "Param", "field": "param", "align": "left"},
283
+ {
284
+ "name": "label",
285
+ "label": "Payload",
286
+ "field": "label",
287
+ "align": "left",
288
+ },
289
+ {"name": "value", "label": "Value", "field": "value", "align": "right"},
290
+ {
291
+ "name": "status",
292
+ "label": "Status",
293
+ "field": "status_code",
294
+ "align": "center",
295
+ },
296
+ {"name": "ms", "label": "ms", "field": "duration_ms", "align": "right"},
297
+ {
298
+ "name": "suspicious",
299
+ "label": "⚠",
300
+ "field": "suspicious",
301
+ "align": "center",
302
+ },
303
+ ],
304
+ rows=[],
305
+ row_key="label",
306
+ )
307
+ .classes("w-full")
308
+ .props("dense flat dark")
309
+ )
310
+ overflow_table.add_slot(
311
+ "body-cell-suspicious",
312
+ """
313
+ <q-td :props="props">
314
+ <q-badge v-if="props.value" color="negative" label="!" />
315
+ </q-td>
316
+ """,
317
+ )
318
+
319
+ async def _run_overflow() -> None:
320
+ from pypproxy.security.int_overflow import run_checks as overflow_run_checks
321
+
322
+ entry = state.get("entry")
323
+ if not entry:
324
+ ui.notify("No entry selected", type="warning")
325
+ return
326
+ overflow_run_btn.props("loading")
327
+ try:
328
+ results = await overflow_run_checks(entry)
329
+ if not results:
330
+ ui.notify("No integer parameters found in this entry", type="info")
331
+ return
332
+ overflow_table.rows = [r.to_dict() for r in results]
333
+ overflow_table.update()
334
+ suspicious = sum(1 for r in results if r.suspicious)
335
+ overflow_label.text = (
336
+ f"{len(results)} tests — {suspicious} suspicious (5xx responses)"
337
+ )
338
+ if suspicious:
339
+ ui.notify(f"⚠ {suspicious} server errors triggered!", type="warning")
340
+ else:
341
+ ui.notify("Done", type="positive")
342
+ finally:
343
+ overflow_run_btn.props(remove="loading")
344
+
345
+ overflow_run_btn.on("click", _run_overflow)
346
+
347
+ def open_entry(entry: Entry) -> None:
348
+ state["entry"] = entry
349
+ entry_label.text = f"#{entry.id} {entry.method} {entry.scheme}://{entry.host}{entry.path}"
350
+ # Auto-fill JWT token
351
+ token = extract_jwt_from_headers(entry.req_headers)
352
+ if token:
353
+ token_input.value = token
354
+ rand_token_input.value = token
355
+
356
+ return {"open_entry": open_entry}