hullproof 0.0.0-stage → 0.5.0

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 (363) hide show
  1. package/LICENSE +222 -0
  2. package/NOTICE +32 -0
  3. package/README.md +313 -2
  4. package/checks/db.firebase-rules-not-open.json +24 -0
  5. package/checks/db.rls-every-table.json +25 -0
  6. package/checks/log.no-credential-in-logs.json +30 -0
  7. package/checks/secrets.no-env-file-committed.json +31 -0
  8. package/checks/secrets.no-secret-in-public-env.json +37 -0
  9. package/checks/storage.firebase-rules-not-open.json +22 -0
  10. package/checks/supply.no-untrusted-event-in-run.json +23 -0
  11. package/checks/web.cors-no-reflected-origin.json +31 -0
  12. package/detectors/ai.json +25 -0
  13. package/detectors/auth.json +27 -0
  14. package/detectors/ci.json +19 -0
  15. package/detectors/containers.json +8 -0
  16. package/detectors/data_stores.json +38 -0
  17. package/detectors/frameworks.json +94 -0
  18. package/detectors/hosting.json +44 -0
  19. package/detectors/iac.json +43 -0
  20. package/detectors/languages.json +48 -0
  21. package/detectors/mobile.json +22 -0
  22. package/detectors/package_managers.json +39 -0
  23. package/detectors/payments.json +19 -0
  24. package/detectors/runtimes.json +17 -0
  25. package/dist/checks/code/admin-role.js +46 -0
  26. package/dist/checks/code/owner-filter.js +91 -0
  27. package/dist/checks/code/shared.js +84 -0
  28. package/dist/checks/code/webhook-verify.js +77 -0
  29. package/dist/checks/context.js +64 -0
  30. package/dist/checks/map/config-checks.js +74 -0
  31. package/dist/checks/map/entry-checks.js +97 -0
  32. package/dist/checks/map/shared.js +44 -0
  33. package/dist/checks/ops.js +307 -0
  34. package/dist/checks/pro-map.js +1 -0
  35. package/dist/checks/run.js +92 -0
  36. package/dist/checks/types.js +1 -0
  37. package/dist/ci/comment.js +44 -0
  38. package/dist/ci/sarif-check.js +157 -0
  39. package/dist/ci/sarif.js +61 -0
  40. package/dist/cli.js +358 -0
  41. package/dist/commands/ci.js +115 -0
  42. package/dist/commands/git.js +58 -0
  43. package/dist/commands/hooks.js +30 -0
  44. package/dist/commands/hunt-pro.js +1 -0
  45. package/dist/commands/hunt.js +86 -0
  46. package/dist/commands/map.js +33 -0
  47. package/dist/commands/mcp.js +17 -0
  48. package/dist/commands/pack.js +48 -0
  49. package/dist/commands/prove-pro.js +1 -0
  50. package/dist/commands/prove.js +173 -0
  51. package/dist/commands/report-gate.js +92 -0
  52. package/dist/commands/scan.js +158 -0
  53. package/dist/confirm/agreement.js +18 -0
  54. package/dist/confirm/chain.js +84 -0
  55. package/dist/confirm/confirm.js +112 -0
  56. package/dist/confirm/honesty.js +75 -0
  57. package/dist/confirm/lead.js +92 -0
  58. package/dist/confirm/merge.js +88 -0
  59. package/dist/confirm/query.js +145 -0
  60. package/dist/confirm/rule.js +86 -0
  61. package/dist/confirm/store.js +42 -0
  62. package/dist/confirm/summary.js +6 -0
  63. package/dist/confirm/synthesis.js +28 -0
  64. package/dist/discover/detectors.js +50 -0
  65. package/dist/discover/discover.js +270 -0
  66. package/dist/discover/extract.js +282 -0
  67. package/dist/discover/git.js +40 -0
  68. package/dist/discover/kit-files.js +47 -0
  69. package/dist/discover/walk.js +110 -0
  70. package/dist/env.js +57 -0
  71. package/dist/gate/gate.js +173 -0
  72. package/dist/hooks/install.js +153 -0
  73. package/dist/hooks/run.js +112 -0
  74. package/dist/map/analyze.js +201 -0
  75. package/dist/map/build.js +253 -0
  76. package/dist/map/candidates.js +49 -0
  77. package/dist/map/cblocks.js +77 -0
  78. package/dist/map/config.js +291 -0
  79. package/dist/map/dotnet.js +242 -0
  80. package/dist/map/entry.js +78 -0
  81. package/dist/map/expo.js +190 -0
  82. package/dist/map/go.js +292 -0
  83. package/dist/map/js-express.js +143 -0
  84. package/dist/map/js-misc.js +295 -0
  85. package/dist/map/js-next.js +292 -0
  86. package/dist/map/laravel.js +389 -0
  87. package/dist/map/lex-more.js +305 -0
  88. package/dist/map/lex.js +425 -0
  89. package/dist/map/light-index.js +191 -0
  90. package/dist/map/light-profiles.js +324 -0
  91. package/dist/map/light.js +574 -0
  92. package/dist/map/local-vocab.js +27 -0
  93. package/dist/map/mobile.js +326 -0
  94. package/dist/map/py-django.js +281 -0
  95. package/dist/map/py.js +391 -0
  96. package/dist/map/rails.js +391 -0
  97. package/dist/map/repo.js +178 -0
  98. package/dist/map/rust.js +312 -0
  99. package/dist/map/spring.js +207 -0
  100. package/dist/map/taint.js +96 -0
  101. package/dist/map/touches.js +355 -0
  102. package/dist/map/vocab.js +88 -0
  103. package/dist/mapping/controls.js +34 -0
  104. package/dist/mapping/dependency-policy.js +44 -0
  105. package/dist/mapping/map.js +44 -0
  106. package/dist/mapping/maps/gitleaks.json +12 -0
  107. package/dist/mapping/maps/osv-scanner.json +7 -0
  108. package/dist/mapping/maps/trivy.json +13 -0
  109. package/dist/mcp/confine.js +59 -0
  110. package/dist/mcp/protocol.js +140 -0
  111. package/dist/mcp/schema.js +43 -0
  112. package/dist/mcp/server.js +37 -0
  113. package/dist/mcp/tools.js +238 -0
  114. package/dist/packs/coverage.js +79 -0
  115. package/dist/packs/manifest.js +36 -0
  116. package/dist/packs/validate.js +271 -0
  117. package/dist/prove/container.js +276 -0
  118. package/dist/prove/exec-main.js +20 -0
  119. package/dist/prove/executor.js +149 -0
  120. package/dist/prove/local.js +131 -0
  121. package/dist/prove/probes.js +65 -0
  122. package/dist/prove/recipe.js +278 -0
  123. package/dist/prove/run.js +136 -0
  124. package/dist/prove/runner.js +1 -0
  125. package/dist/prove/safety.js +126 -0
  126. package/dist/prove/spec.js +357 -0
  127. package/dist/prove/store.js +65 -0
  128. package/dist/report/build.js +297 -0
  129. package/dist/report/entry-points.js +75 -0
  130. package/dist/report/hunt-section.js +71 -0
  131. package/dist/report/kit.js +41 -0
  132. package/dist/report/load.js +35 -0
  133. package/dist/report/messages.js +273 -0
  134. package/dist/report/prove-section.js +65 -0
  135. package/dist/report/report.js +156 -0
  136. package/dist/report/surface.js +21 -0
  137. package/dist/report/test-code.js +51 -0
  138. package/dist/report/tiers.js +44 -0
  139. package/dist/scanners/common.js +162 -0
  140. package/dist/scanners/gitleaks.js +90 -0
  141. package/dist/scanners/index.js +113 -0
  142. package/dist/scanners/osv-scanner.js +77 -0
  143. package/dist/scanners/run-targets.js +34 -0
  144. package/dist/scanners/secret-noise.js +89 -0
  145. package/dist/scanners/semgrep.js +142 -0
  146. package/dist/scanners/trivy.js +65 -0
  147. package/dist/scanners/types.js +1 -0
  148. package/dist/types/check.js +1 -0
  149. package/dist/types/codemap.js +1 -0
  150. package/dist/types/findings.js +5 -0
  151. package/dist/types/hunt.js +1 -0
  152. package/dist/types/index.js +1 -0
  153. package/dist/types/prove.js +1 -0
  154. package/dist/types/results.js +2 -0
  155. package/dist/types/scan.js +1 -0
  156. package/dist/types/stack.js +5 -0
  157. package/dist/util.js +71 -0
  158. package/dist/version.js +2 -0
  159. package/package.json +46 -4
  160. package/packs/android/fixture-result.json +43 -0
  161. package/packs/android/pack.json +56 -0
  162. package/packs/dotnet/fixture-result.json +62 -0
  163. package/packs/dotnet/pack.json +88 -0
  164. package/packs/expo/answer-key.json +51 -0
  165. package/packs/expo/fixture-result.json +62 -0
  166. package/packs/expo/pack.json +79 -0
  167. package/packs/firebase/answer-key.json +27 -0
  168. package/packs/firebase/fixture-result.json +43 -0
  169. package/packs/firebase/pack.json +58 -0
  170. package/packs/go/answer-key.json +101 -0
  171. package/packs/go/fixture-result.json +64 -0
  172. package/packs/go/pack.json +99 -0
  173. package/packs/ios/fixture-result.json +43 -0
  174. package/packs/ios/pack.json +55 -0
  175. package/packs/laravel/answer-key.json +84 -0
  176. package/packs/laravel/fixture-result.json +60 -0
  177. package/packs/laravel/pack.json +91 -0
  178. package/packs/node-ts/answer-key.json +220 -0
  179. package/packs/node-ts/fixture-result.json +114 -0
  180. package/packs/node-ts/pack.json +134 -0
  181. package/packs/python/answer-key.json +195 -0
  182. package/packs/python/fixture-result.json +119 -0
  183. package/packs/python/pack.json +121 -0
  184. package/packs/rails/answer-key.json +84 -0
  185. package/packs/rails/fixture-result.json +60 -0
  186. package/packs/rails/pack.json +78 -0
  187. package/packs/rust/fixture-result.json +43 -0
  188. package/packs/rust/pack.json +69 -0
  189. package/packs/spring/fixture-result.json +63 -0
  190. package/packs/spring/pack.json +103 -0
  191. package/packs/supabase/answer-key.json +52 -0
  192. package/packs/supabase/fixture-result.json +58 -0
  193. package/packs/supabase/pack.json +60 -0
  194. package/rules/gitleaks.toml +256 -0
  195. package/rules/semgrep/api-route-no-auth.yaml +31 -0
  196. package/rules/semgrep/cors-reflect-origin.yaml +48 -0
  197. package/rules/semgrep/eval-or-new-function.yaml +185 -0
  198. package/rules/semgrep/log-session-token.yaml +48 -0
  199. package/rules/semgrep/math-random-tokens.yaml +24 -0
  200. package/rules/semgrep/next-public-secret-name.yaml +32 -0
  201. package/rules/semgrep/postgrest-filter-injection.yaml +78 -0
  202. package/rules/semgrep/raw-sql-concat.yaml +42 -0
  203. package/rules/semgrep/read-by-id-no-owner.yaml +49 -0
  204. package/rules/semgrep/server-action-no-auth.yaml +26 -0
  205. package/rules/semgrep/sql-variants.yaml +99 -0
  206. package/rules/semgrep/ssrf-unguarded-url.yaml +137 -0
  207. package/rules/semgrep/subprocess-injection.yaml +163 -0
  208. package/rules/semgrep/taint-go.yaml +333 -0
  209. package/rules/semgrep/taint-java.yaml +317 -0
  210. package/rules/semgrep/taint-javascript.yaml +412 -0
  211. package/rules/semgrep/taint-php.yaml +361 -0
  212. package/rules/semgrep/taint-python.yaml +414 -0
  213. package/rules/semgrep/tool-endpoint-admin-client.yaml +86 -0
  214. package/rules/semgrep/user-metadata-authz.yaml +22 -0
  215. package/rules/semgrep/verify-result-unused.yaml +62 -0
  216. package/rules/semgrep/webhook-parse-before-verify.yaml +29 -0
  217. package/rules/test-artifact-ids.json +4 -0
  218. package/rules/tiers.json +427 -0
  219. package/schemas/check-results.schema.json +55 -0
  220. package/schemas/check.schema.json +41 -0
  221. package/schemas/codemap.schema.json +216 -0
  222. package/schemas/findings.schema.json +1048 -0
  223. package/schemas/hunt-output.schema.json +46 -0
  224. package/schemas/hunt-result.schema.json +58 -0
  225. package/schemas/results.schema.json +17 -0
  226. package/schemas/scan-hits.schema.json +32 -0
  227. package/schemas/scan-meta.schema.json +25 -0
  228. package/schemas/stack.schema.json +93 -0
  229. package/src/checks/README.md +32 -0
  230. package/src/checks/code/admin-role.ts +49 -0
  231. package/src/checks/code/owner-filter.ts +95 -0
  232. package/src/checks/code/shared.ts +66 -0
  233. package/src/checks/code/webhook-verify.ts +75 -0
  234. package/src/checks/context.ts +72 -0
  235. package/src/checks/map/config-checks.ts +79 -0
  236. package/src/checks/map/entry-checks.ts +116 -0
  237. package/src/checks/map/shared.ts +63 -0
  238. package/src/checks/ops.ts +280 -0
  239. package/src/checks/pro-map.ts +4 -0
  240. package/src/checks/run.ts +80 -0
  241. package/src/checks/types.ts +12 -0
  242. package/src/ci/comment.ts +52 -0
  243. package/src/ci/sarif-check.ts +100 -0
  244. package/src/ci/sarif.ts +72 -0
  245. package/src/cli.ts +273 -0
  246. package/src/commands/ci.ts +104 -0
  247. package/src/commands/git.ts +59 -0
  248. package/src/commands/hooks.ts +27 -0
  249. package/src/commands/hunt-pro.ts +4 -0
  250. package/src/commands/hunt.ts +68 -0
  251. package/src/commands/map.ts +27 -0
  252. package/src/commands/mcp.ts +18 -0
  253. package/src/commands/pack.ts +34 -0
  254. package/src/commands/prove-pro.ts +4 -0
  255. package/src/commands/prove.ts +126 -0
  256. package/src/commands/report-gate.ts +75 -0
  257. package/src/commands/scan.ts +156 -0
  258. package/src/confirm/agreement.ts +23 -0
  259. package/src/confirm/chain.ts +67 -0
  260. package/src/confirm/confirm.ts +107 -0
  261. package/src/confirm/honesty.ts +65 -0
  262. package/src/confirm/lead.ts +76 -0
  263. package/src/confirm/merge.ts +89 -0
  264. package/src/confirm/query.ts +126 -0
  265. package/src/confirm/rule.ts +76 -0
  266. package/src/confirm/store.ts +42 -0
  267. package/src/confirm/summary.ts +9 -0
  268. package/src/confirm/synthesis.ts +33 -0
  269. package/src/discover/detectors.ts +71 -0
  270. package/src/discover/discover.ts +253 -0
  271. package/src/discover/extract.ts +235 -0
  272. package/src/discover/git.ts +31 -0
  273. package/src/discover/kit-files.ts +37 -0
  274. package/src/discover/walk.ts +88 -0
  275. package/src/env.ts +51 -0
  276. package/src/gate/gate.ts +169 -0
  277. package/src/hooks/install.ts +142 -0
  278. package/src/hooks/run.ts +121 -0
  279. package/src/map/analyze.ts +219 -0
  280. package/src/map/build.ts +206 -0
  281. package/src/map/candidates.ts +47 -0
  282. package/src/map/cblocks.ts +55 -0
  283. package/src/map/config.ts +207 -0
  284. package/src/map/dotnet.ts +205 -0
  285. package/src/map/entry.ts +80 -0
  286. package/src/map/expo.ts +171 -0
  287. package/src/map/go.ts +271 -0
  288. package/src/map/js-express.ts +135 -0
  289. package/src/map/js-misc.ts +221 -0
  290. package/src/map/js-next.ts +282 -0
  291. package/src/map/laravel.ts +327 -0
  292. package/src/map/lex-more.ts +152 -0
  293. package/src/map/lex.ts +274 -0
  294. package/src/map/light-index.ts +134 -0
  295. package/src/map/light-profiles.ts +363 -0
  296. package/src/map/light.ts +484 -0
  297. package/src/map/local-vocab.ts +30 -0
  298. package/src/map/mobile.ts +263 -0
  299. package/src/map/py-django.ts +239 -0
  300. package/src/map/py.ts +329 -0
  301. package/src/map/rails.ts +291 -0
  302. package/src/map/repo.ts +150 -0
  303. package/src/map/rust.ts +256 -0
  304. package/src/map/spring.ts +185 -0
  305. package/src/map/taint.ts +86 -0
  306. package/src/map/touches.ts +296 -0
  307. package/src/map/vocab.ts +106 -0
  308. package/src/mapping/controls.ts +47 -0
  309. package/src/mapping/dependency-policy.ts +55 -0
  310. package/src/mapping/map.ts +59 -0
  311. package/src/mapping/maps/gitleaks.json +12 -0
  312. package/src/mapping/maps/osv-scanner.json +7 -0
  313. package/src/mapping/maps/trivy.json +13 -0
  314. package/src/mcp/confine.ts +44 -0
  315. package/src/mcp/protocol.ts +144 -0
  316. package/src/mcp/schema.ts +35 -0
  317. package/src/mcp/server.ts +32 -0
  318. package/src/mcp/tools.ts +235 -0
  319. package/src/packs/coverage.ts +98 -0
  320. package/src/packs/manifest.ts +97 -0
  321. package/src/packs/validate.ts +204 -0
  322. package/src/prove/container.ts +237 -0
  323. package/src/prove/exec-main.ts +16 -0
  324. package/src/prove/executor.ts +130 -0
  325. package/src/prove/local.ts +100 -0
  326. package/src/prove/probes.ts +68 -0
  327. package/src/prove/recipe.ts +215 -0
  328. package/src/prove/run.ts +124 -0
  329. package/src/prove/runner.ts +28 -0
  330. package/src/prove/safety.ts +98 -0
  331. package/src/prove/spec.ts +256 -0
  332. package/src/prove/store.ts +48 -0
  333. package/src/report/build.ts +304 -0
  334. package/src/report/entry-points.ts +78 -0
  335. package/src/report/hunt-section.ts +67 -0
  336. package/src/report/kit.ts +59 -0
  337. package/src/report/load.ts +49 -0
  338. package/src/report/messages.ts +270 -0
  339. package/src/report/prove-section.ts +61 -0
  340. package/src/report/report.ts +160 -0
  341. package/src/report/surface.ts +32 -0
  342. package/src/report/test-code.ts +56 -0
  343. package/src/report/tiers.ts +50 -0
  344. package/src/scanners/common.ts +149 -0
  345. package/src/scanners/gitleaks.ts +89 -0
  346. package/src/scanners/index.ts +103 -0
  347. package/src/scanners/osv-scanner.ts +90 -0
  348. package/src/scanners/run-targets.ts +35 -0
  349. package/src/scanners/secret-noise.ts +80 -0
  350. package/src/scanners/semgrep.ts +139 -0
  351. package/src/scanners/trivy.ts +66 -0
  352. package/src/scanners/types.ts +98 -0
  353. package/src/types/check.ts +52 -0
  354. package/src/types/codemap.ts +142 -0
  355. package/src/types/findings.ts +107 -0
  356. package/src/types/hunt.ts +97 -0
  357. package/src/types/index.ts +10 -0
  358. package/src/types/prove.ts +83 -0
  359. package/src/types/results.ts +15 -0
  360. package/src/types/scan.ts +63 -0
  361. package/src/types/stack.ts +61 -0
  362. package/src/util.ts +59 -0
  363. package/src/version.ts +2 -0
@@ -0,0 +1,414 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Rare Phronesis Limited
3
+ # Written for the Hullproof Engine from the CWE definitions named in each rule. Not copied or derived from any Semgrep
4
+ # registry rule. Fixtures: test/fixtures/rules/taint-python.py, run by test/rules.test.ts with semgrep --test.
5
+ #
6
+ # Sources are request data in Flask, Django (and Django REST framework) and FastAPI (and Starlette): the request object's
7
+ # fields and the parameters of a function under a route decorator. Taint flows through string building, assignment and
8
+ # calls by default. Each rule names its sanitizers; a project guard is recognised when its name says what it checks.
9
+ rules:
10
+ - id: hullproof-taint-py-sql-injection
11
+ languages: [python]
12
+ mode: taint
13
+ severity: ERROR
14
+ message: >-
15
+ Request data reaches the text of a SQL query (CWE-89). Pass the value as a bound parameter, for example
16
+ cursor.execute("SELECT * FROM t WHERE id = %s", (value,)), and keep the query text constant.
17
+ metadata:
18
+ cwe: "CWE-89: Improper Neutralization of Special Elements used in an SQL Command"
19
+ sec_ids: [SEC-API-017]
20
+ pattern-sources:
21
+ - pattern: flask.request
22
+ - patterns:
23
+ - pattern-either:
24
+ - pattern: $REQ.GET
25
+ - pattern: $REQ.POST
26
+ - pattern: $REQ.COOKIES
27
+ - pattern: $REQ.META
28
+ - pattern: $REQ.FILES
29
+ - pattern: $REQ.body
30
+ - pattern: $REQ.headers
31
+ - pattern: $REQ.cookies
32
+ - pattern: $REQ.data
33
+ - pattern: $REQ.query_params
34
+ - pattern: $REQ.path_params
35
+ - pattern: $REQ.json()
36
+ - pattern: $REQ.body()
37
+ - pattern: $REQ.form()
38
+ - metavariable-regex:
39
+ metavariable: $REQ
40
+ regex: ^(request|req|self\.request)$
41
+ - patterns:
42
+ - pattern-either:
43
+ - pattern-inside: |
44
+ @$APP.$VERB(...)
45
+ def $F(..., $P, ...):
46
+ ...
47
+ - pattern-inside: |
48
+ @$APP.$VERB(...)
49
+ async def $F(..., $P, ...):
50
+ ...
51
+ - metavariable-regex:
52
+ metavariable: $VERB
53
+ regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
54
+ - focus-metavariable: $P
55
+ pattern-sanitizers:
56
+ - pattern-either:
57
+ - pattern: int(...)
58
+ - pattern: float(...)
59
+ - pattern: bool(...)
60
+ - pattern: uuid.UUID(...)
61
+ pattern-sinks:
62
+ - patterns:
63
+ - pattern-either:
64
+ - pattern: $C.execute($Q, ...)
65
+ - pattern: $C.executemany($Q, ...)
66
+ - pattern: $C.executescript($Q, ...)
67
+ - pattern: $C.exec_driver_sql($Q, ...)
68
+ - pattern: $M.objects.raw($Q, ...)
69
+ - pattern: $M.objects.extra(..., where=$Q, ...)
70
+ - pattern: django.db.models.expressions.RawSQL($Q, ...)
71
+ - pattern: sqlalchemy.text($Q, ...)
72
+ - pattern: pandas.read_sql($Q, ...)
73
+ - pattern: pandas.read_sql_query($Q, ...)
74
+ - focus-metavariable: $Q
75
+
76
+ - id: hullproof-taint-py-xss
77
+ languages: [python]
78
+ mode: taint
79
+ severity: ERROR
80
+ message: >-
81
+ Request data is written into an HTML response without encoding (CWE-79). Render it through an autoescaping template
82
+ (render_template, Django templates) or encode it with html.escape or markupsafe.escape before it is placed in HTML.
83
+ metadata:
84
+ cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation"
85
+ sec_ids: [SEC-WEB-031]
86
+ pattern-sources:
87
+ - pattern: flask.request
88
+ - patterns:
89
+ - pattern-either:
90
+ - pattern: $REQ.GET
91
+ - pattern: $REQ.POST
92
+ - pattern: $REQ.COOKIES
93
+ - pattern: $REQ.META
94
+ - pattern: $REQ.FILES
95
+ - pattern: $REQ.body
96
+ - pattern: $REQ.headers
97
+ - pattern: $REQ.cookies
98
+ - pattern: $REQ.data
99
+ - pattern: $REQ.query_params
100
+ - pattern: $REQ.path_params
101
+ - pattern: $REQ.json()
102
+ - pattern: $REQ.body()
103
+ - pattern: $REQ.form()
104
+ - metavariable-regex:
105
+ metavariable: $REQ
106
+ regex: ^(request|req|self\.request)$
107
+ - patterns:
108
+ - pattern-either:
109
+ - pattern-inside: |
110
+ @$APP.$VERB(...)
111
+ def $F(..., $P, ...):
112
+ ...
113
+ - pattern-inside: |
114
+ @$APP.$VERB(...)
115
+ async def $F(..., $P, ...):
116
+ ...
117
+ - metavariable-regex:
118
+ metavariable: $VERB
119
+ regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
120
+ - focus-metavariable: $P
121
+ pattern-sanitizers:
122
+ - pattern-either:
123
+ - pattern: html.escape(...)
124
+ - pattern: markupsafe.escape(...)
125
+ - pattern: flask.escape(...)
126
+ - pattern: django.utils.html.escape(...)
127
+ - pattern: django.utils.html.conditional_escape(...)
128
+ - pattern: bleach.clean(...)
129
+ - pattern: flask.jsonify(...)
130
+ - pattern: flask.render_template(...)
131
+ - pattern: django.shortcuts.render(...)
132
+ - pattern: flask.redirect(...)
133
+ - pattern: flask.url_for(...)
134
+ - pattern: flask.send_file(...)
135
+ - pattern: flask.send_from_directory(...)
136
+ - pattern: urllib.parse.quote(...)
137
+ - pattern: int(...)
138
+ - pattern: float(...)
139
+ - pattern: bool(...)
140
+ - pattern: len(...)
141
+ pattern-sinks:
142
+ # A Flask view's return value becomes a text/html response when it is a string. Dict and list returns become JSON.
143
+ - patterns:
144
+ - pattern-inside: |
145
+ @$APP.route(...)
146
+ def $F(...):
147
+ ...
148
+ - pattern-either:
149
+ - pattern: return $X
150
+ - pattern: return $X, ...
151
+ - pattern-not: return {...}
152
+ - pattern-not: return [...]
153
+ - pattern-not: return {...}, ...
154
+ - focus-metavariable: $X
155
+ - patterns:
156
+ - pattern-either:
157
+ - pattern: flask.make_response($X, ...)
158
+ - pattern: flask.Response($X, ...)
159
+ - pattern: flask.render_template_string($X, ...)
160
+ - pattern: markupsafe.Markup($X)
161
+ - pattern: django.http.HttpResponse($X, ...)
162
+ - pattern: django.utils.safestring.mark_safe($X)
163
+ - pattern: django.utils.html.format_html($X, ...)
164
+ - pattern: fastapi.responses.HTMLResponse($X, ...)
165
+ - pattern: starlette.responses.HTMLResponse($X, ...)
166
+ - pattern-not: flask.Response($X, ..., mimetype="application/json", ...)
167
+ - pattern-not: django.http.HttpResponse($X, ..., content_type="application/json", ...)
168
+ - focus-metavariable: $X
169
+
170
+ - id: hullproof-taint-py-command-injection
171
+ languages: [python]
172
+ mode: taint
173
+ severity: ERROR
174
+ message: >-
175
+ Request data reaches a shell command or chooses the program that runs (CWE-78). Run a fixed program with an
176
+ argument list and no shell, map the request value to a fixed set of options, or quote it with shlex.quote when a
177
+ shell cannot be avoided.
178
+ metadata:
179
+ cwe: "CWE-78: Improper Neutralization of Special Elements used in an OS Command"
180
+ sec_ids: [SEC-API-020]
181
+ pattern-sources:
182
+ - pattern: flask.request
183
+ - patterns:
184
+ - pattern-either:
185
+ - pattern: $REQ.GET
186
+ - pattern: $REQ.POST
187
+ - pattern: $REQ.COOKIES
188
+ - pattern: $REQ.META
189
+ - pattern: $REQ.FILES
190
+ - pattern: $REQ.body
191
+ - pattern: $REQ.headers
192
+ - pattern: $REQ.cookies
193
+ - pattern: $REQ.data
194
+ - pattern: $REQ.query_params
195
+ - pattern: $REQ.path_params
196
+ - pattern: $REQ.json()
197
+ - pattern: $REQ.body()
198
+ - pattern: $REQ.form()
199
+ - metavariable-regex:
200
+ metavariable: $REQ
201
+ regex: ^(request|req|self\.request)$
202
+ - patterns:
203
+ - pattern-either:
204
+ - pattern-inside: |
205
+ @$APP.$VERB(...)
206
+ def $F(..., $P, ...):
207
+ ...
208
+ - pattern-inside: |
209
+ @$APP.$VERB(...)
210
+ async def $F(..., $P, ...):
211
+ ...
212
+ - metavariable-regex:
213
+ metavariable: $VERB
214
+ regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
215
+ - focus-metavariable: $P
216
+ pattern-sanitizers:
217
+ - pattern-either:
218
+ - pattern: shlex.quote(...)
219
+ - pattern: pipes.quote(...)
220
+ - pattern: int(...)
221
+ - pattern: float(...)
222
+ pattern-sinks:
223
+ - patterns:
224
+ - pattern-either:
225
+ - pattern: os.system($X)
226
+ - pattern: os.popen($X, ...)
227
+ - pattern: subprocess.getoutput($X)
228
+ - pattern: subprocess.getstatusoutput($X)
229
+ - pattern: asyncio.create_subprocess_shell($X, ...)
230
+ - pattern: subprocess.$F($X, ..., shell=True, ...)
231
+ - pattern: subprocess.$F([$X, ...], ...)
232
+ - pattern: subprocess.$F([$SH, "-c", $X, ...], ...)
233
+ - patterns:
234
+ - pattern: os.$EXEC($X, ...)
235
+ - metavariable-regex:
236
+ metavariable: $EXEC
237
+ regex: ^(execl|execle|execlp|execv|execve|execvp|execvpe|spawnl|spawnlp|spawnv|spawnvp)$
238
+ - focus-metavariable: $X
239
+ - patterns:
240
+ - pattern: subprocess.$F($X, ...)
241
+ - pattern-not: subprocess.$F([...], ...)
242
+ - metavariable-regex:
243
+ metavariable: $F
244
+ regex: ^(run|call|check_call|check_output|Popen)$
245
+ - focus-metavariable: $X
246
+
247
+ - id: hullproof-taint-py-path-traversal
248
+ languages: [python]
249
+ mode: taint
250
+ severity: WARNING
251
+ message: >-
252
+ Request data becomes part of a file path the server opens, writes, lists or deletes (CWE-22). Reduce the name with
253
+ os.path.basename or werkzeug secure_filename, or resolve the full path and check that it stays inside a fixed folder
254
+ before using it.
255
+ metadata:
256
+ cwe: "CWE-22: Improper Limitation of a Pathname to a Restricted Directory"
257
+ sec_ids: []
258
+ pattern-sources:
259
+ - pattern: flask.request
260
+ - patterns:
261
+ - pattern-either:
262
+ - pattern: $REQ.GET
263
+ - pattern: $REQ.POST
264
+ - pattern: $REQ.COOKIES
265
+ - pattern: $REQ.META
266
+ - pattern: $REQ.FILES
267
+ - pattern: $REQ.body
268
+ - pattern: $REQ.headers
269
+ - pattern: $REQ.cookies
270
+ - pattern: $REQ.data
271
+ - pattern: $REQ.query_params
272
+ - pattern: $REQ.path_params
273
+ - pattern: $REQ.json()
274
+ - pattern: $REQ.body()
275
+ - pattern: $REQ.form()
276
+ - metavariable-regex:
277
+ metavariable: $REQ
278
+ regex: ^(request|req|self\.request)$
279
+ - patterns:
280
+ - pattern-either:
281
+ - pattern-inside: |
282
+ @$APP.$VERB(...)
283
+ def $F(..., $P, ...):
284
+ ...
285
+ - pattern-inside: |
286
+ @$APP.$VERB(...)
287
+ async def $F(..., $P, ...):
288
+ ...
289
+ - metavariable-regex:
290
+ metavariable: $VERB
291
+ regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
292
+ - focus-metavariable: $P
293
+ pattern-sanitizers:
294
+ - pattern-either:
295
+ - pattern: os.path.basename(...)
296
+ - pattern: werkzeug.utils.secure_filename(...)
297
+ - pattern: werkzeug.security.safe_join(...)
298
+ - pattern: werkzeug.utils.safe_join(...)
299
+ - pattern: flask.safe_join(...)
300
+ - pattern: int(...)
301
+ - pattern: uuid.UUID(...)
302
+ - by-side-effect: true
303
+ patterns:
304
+ - pattern: $GUARD($X, ...)
305
+ - metavariable-regex:
306
+ metavariable: $GUARD
307
+ regex: (?i).*(safe|allow|valid|guard|check|verify|within|inside|contain).*
308
+ - focus-metavariable: $X
309
+ pattern-sinks:
310
+ - patterns:
311
+ - pattern-either:
312
+ - pattern: open($X, ...)
313
+ - pattern: io.open($X, ...)
314
+ - pattern: codecs.open($X, ...)
315
+ - pattern: pathlib.Path($X, ...)
316
+ - pattern: pathlib.Path($BASE, $X, ...)
317
+ - pattern: flask.send_file($X, ...)
318
+ - patterns:
319
+ - pattern: os.$OSF($X, ...)
320
+ - metavariable-regex:
321
+ metavariable: $OSF
322
+ regex: ^(remove|unlink|rmdir|removedirs|mkdir|makedirs|listdir|scandir|stat|chmod|chown|rename|replace|walk|open|truncate)$
323
+ - patterns:
324
+ - pattern-either:
325
+ - pattern: shutil.$SHF($X, ...)
326
+ - pattern: shutil.$SHF($SRC, $X, ...)
327
+ - metavariable-regex:
328
+ metavariable: $SHF
329
+ regex: ^(copy|copyfile|copy2|copytree|move|rmtree)$
330
+ - focus-metavariable: $X
331
+
332
+ - id: hullproof-taint-py-ssrf
333
+ languages: [python]
334
+ mode: taint
335
+ severity: ERROR
336
+ message: >-
337
+ Request data chooses the address of an outbound request the server makes (CWE-918). Allow only http and https,
338
+ resolve the host, refuse private, loopback, link local and metadata addresses, connect to the checked address and
339
+ do not follow redirects unchecked. A value that only fills a path or query under a fixed host is not matched.
340
+ metadata:
341
+ cwe: "CWE-918: Server-Side Request Forgery (SSRF)"
342
+ sec_ids: [SEC-API-035]
343
+ pattern-sources:
344
+ - pattern: flask.request
345
+ - patterns:
346
+ - pattern-either:
347
+ - pattern: $REQ.GET
348
+ - pattern: $REQ.POST
349
+ - pattern: $REQ.COOKIES
350
+ - pattern: $REQ.META
351
+ - pattern: $REQ.FILES
352
+ - pattern: $REQ.body
353
+ - pattern: $REQ.headers
354
+ - pattern: $REQ.cookies
355
+ - pattern: $REQ.data
356
+ - pattern: $REQ.query_params
357
+ - pattern: $REQ.path_params
358
+ - pattern: $REQ.json()
359
+ - pattern: $REQ.body()
360
+ - pattern: $REQ.form()
361
+ - metavariable-regex:
362
+ metavariable: $REQ
363
+ regex: ^(request|req|self\.request)$
364
+ - patterns:
365
+ - pattern-either:
366
+ - pattern-inside: |
367
+ @$APP.$VERB(...)
368
+ def $F(..., $P, ...):
369
+ ...
370
+ - pattern-inside: |
371
+ @$APP.$VERB(...)
372
+ async def $F(..., $P, ...):
373
+ ...
374
+ - metavariable-regex:
375
+ metavariable: $VERB
376
+ regex: ^(route|get|post|put|patch|delete|api_route|websocket)$
377
+ - focus-metavariable: $P
378
+ pattern-sanitizers:
379
+ - pattern-either:
380
+ - pattern: int(...)
381
+ - pattern: urllib.parse.quote(...)
382
+ - pattern: urllib.parse.urlencode(...)
383
+ # A fixed scheme and host before the request value: the value can only change the path or the query.
384
+ - patterns:
385
+ - pattern: '"$URL" + $Y'
386
+ - metavariable-regex:
387
+ metavariable: $URL
388
+ regex: ^https?://[^/]+/
389
+ - pattern-regex: f["']https?://[^/"'{]+/[^"']*["']
390
+ - by-side-effect: true
391
+ patterns:
392
+ - pattern: $GUARD($X, ...)
393
+ - metavariable-regex:
394
+ metavariable: $GUARD
395
+ regex: (?i).*(safe|allow|valid|guard|check|verify).*
396
+ - focus-metavariable: $X
397
+ pattern-sinks:
398
+ - patterns:
399
+ - pattern-either:
400
+ - patterns:
401
+ - pattern-either:
402
+ - pattern: requests.$M($X, ...)
403
+ - pattern: httpx.$M($X, ...)
404
+ - metavariable-regex:
405
+ metavariable: $M
406
+ regex: ^(get|post|put|patch|delete|head|options)$
407
+ - pattern: requests.request($METHOD, $X, ...)
408
+ - pattern: httpx.request($METHOD, $X, ...)
409
+ - pattern: urllib.request.urlopen($X, ...)
410
+ - pattern: urllib.request.Request($X, ...)
411
+ - pattern: urllib3.PoolManager(...).request($METHOD, $X, ...)
412
+ - pattern: http.client.HTTPConnection($X, ...)
413
+ - pattern: http.client.HTTPSConnection($X, ...)
414
+ - focus-metavariable: $X
@@ -0,0 +1,86 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Rare Phronesis Limited
3
+ # Copied from the Hullproof kit (kit/tools/hullproof/rules/tool-endpoint-admin-client.yaml).
4
+ #
5
+ rules:
6
+ - id: hullproof-tool-handler-admin-client-no-scope
7
+ languages: [typescript, javascript]
8
+ severity: WARNING
9
+ message: >-
10
+ A tool handler (an MCP server tool or a model tool) uses an admin or service role client and
11
+ takes no argument that could carry the caller (the handler has at most one parameter, its input).
12
+ Anyone who can call the tool, or any prompt that makes the model call it, acts with the service
13
+ role's powers. Use a client that carries the caller's own token, or check the caller's scope and
14
+ ownership of every id before using the admin client. Flagged for review.
15
+ # The admin client names below are heuristics. Edit them to match the project. A handler with two or
16
+ # more parameters is not flagged, because the second one (the MCP extra argument, or a context object)
17
+ # is where the caller identity arrives. Read those handlers by hand.
18
+ patterns:
19
+ - pattern-inside:
20
+ pattern-either:
21
+ - pattern: $S.tool(..., async () => { ... }, ...)
22
+ - pattern: $S.tool(..., async ($A) => { ... }, ...)
23
+ - pattern: $S.registerTool(..., async () => { ... }, ...)
24
+ - pattern: $S.registerTool(..., async ($A) => { ... }, ...)
25
+ - pattern: '$S.addTool({..., execute: async () => { ... }, ...})'
26
+ - pattern: '$S.addTool({..., execute: async ($A) => { ... }, ...})'
27
+ - pattern: '$T({..., execute: async () => { ... }, ...})'
28
+ - pattern: '$T({..., execute: async ($A) => { ... }, ...})'
29
+ - pattern: '$T({..., func: async () => { ... }, ...})'
30
+ - pattern: '$T({..., func: async ($A) => { ... }, ...})'
31
+ - pattern: $S.setRequestHandler(CallToolRequestSchema, async () => { ... })
32
+ - pattern: $S.setRequestHandler(CallToolRequestSchema, async ($A) => { ... })
33
+ # A registration made inside a function that takes a parameter (a per request or per user factory)
34
+ # is not flagged: the caller is captured by the closure. Read those by hand.
35
+ - pattern-not-inside: |
36
+ function $F($P, ...) { ... $S.tool(...); ... }
37
+ - pattern-not-inside: |
38
+ async function $F($P, ...) { ... $S.tool(...); ... }
39
+ - pattern-not-inside: |
40
+ ($P, ...) => { ... $S.tool(...); ... }
41
+ - pattern-not-inside: |
42
+ async ($P, ...) => { ... $S.tool(...); ... }
43
+ - pattern-not-inside: |
44
+ function $F($P, ...) { ... $S.registerTool(...); ... }
45
+ - pattern-not-inside: |
46
+ async function $F($P, ...) { ... $S.registerTool(...); ... }
47
+ - pattern-not-inside: |
48
+ ($P, ...) => { ... $S.registerTool(...); ... }
49
+ - pattern-not-inside: |
50
+ async ($P, ...) => { ... $S.registerTool(...); ... }
51
+ - pattern-not-inside: |
52
+ function $F($P, ...) { ... $S.addTool(...); ... }
53
+ - pattern-not-inside: |
54
+ async function $F($P, ...) { ... $S.addTool(...); ... }
55
+ - pattern-not-inside: |
56
+ ($P, ...) => { ... $S.addTool(...); ... }
57
+ - pattern-not-inside: |
58
+ async ($P, ...) => { ... $S.addTool(...); ... }
59
+ - pattern-not-inside: |
60
+ function $F($P, ...) { ... $S.setRequestHandler(...); ... }
61
+ - pattern-not-inside: |
62
+ async function $F($P, ...) { ... $S.setRequestHandler(...); ... }
63
+ - pattern-not-inside: |
64
+ ($P, ...) => { ... $S.setRequestHandler(...); ... }
65
+ - pattern-not-inside: |
66
+ async ($P, ...) => { ... $S.setRequestHandler(...); ... }
67
+ - pattern-either:
68
+ - patterns:
69
+ - pattern: $ADM(...)
70
+ - metavariable-regex:
71
+ metavariable: $ADM
72
+ regex: (?i)^(create|get|make|build|init)?(supabase)?(admin|service|servicerole|privileged)(supabase)?(client|db|database)?$
73
+ - pattern: process.env.SUPABASE_SERVICE_ROLE_KEY
74
+ - pattern: process.env.SERVICE_ROLE_KEY
75
+ - pattern: supabaseAdmin
76
+ - pattern: $X.auth.admin
77
+ - pattern: adminClient
78
+ - pattern: serviceClient
79
+ metadata:
80
+ sec_ids: [SEC-AI-020, SEC-AI-021, SEC-AGENT-008, SEC-SECRETS-003]
81
+ severity_hint: CRITICAL
82
+ confidence: LOW
83
+ note: >-
84
+ Pattern based lead for review. The finding is on the admin client use inside the handler. A caller
85
+ check made before the handler runs (in middleware) is not seen and shows up here, so confirm where
86
+ the caller is authenticated.
@@ -0,0 +1,22 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Rare Phronesis Limited
3
+ # Copied from the Hullproof kit (kit/tools/hullproof/rules/user-metadata-authz.yaml).
4
+ #
5
+ rules:
6
+ - id: hullproof-user-metadata-authz
7
+ languages: [typescript, javascript]
8
+ severity: ERROR
9
+ message: >-
10
+ user_metadata is editable by the signed in user. Do not read it in an authorization decision.
11
+ Use app_metadata or a server owned table.
12
+ pattern-either:
13
+ - pattern: if (<... $U.user_metadata ...>) $S
14
+ - pattern: $U.user_metadata.$F === $V
15
+ - pattern: $U.user_metadata.$F == $V
16
+ - pattern: $U.user_metadata.$F !== $V
17
+ - pattern: $U.user_metadata.$F != $V
18
+ - pattern: '$C ? <... $U.user_metadata ...> : $E'
19
+ metadata:
20
+ sec_ids: [SEC-DB-006, SEC-AUTHZ-005]
21
+ severity_hint: CRITICAL
22
+ confidence: MEDIUM
@@ -0,0 +1,62 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Rare Phronesis Limited
3
+ # Copied from the Hullproof kit (kit/tools/hullproof/rules/verify-result-unused.yaml).
4
+ #
5
+ rules:
6
+ - id: hullproof-verify-result-unused
7
+ languages: [typescript, javascript]
8
+ severity: ERROR
9
+ message: >-
10
+ The result of a signature or token check is computed and then not used to reject the request. The check runs and the
11
+ handler goes on regardless, so a forged request is accepted. Test the result and return an error, or throw, when it is false.
12
+ patterns:
13
+ - pattern-either:
14
+ - patterns:
15
+ - pattern-either:
16
+ - pattern: const $OK = $V(...);
17
+ - pattern: const $OK = await $V(...);
18
+ - pattern: const $OK = $O.$V(...);
19
+ - pattern: const $OK = await $O.$V(...);
20
+ - pattern: let $OK = $V(...);
21
+ - pattern: let $OK = $O.$V(...);
22
+ - pattern-not-inside: |
23
+ {
24
+ ...
25
+ const $OK = ...;
26
+ ...
27
+ <... $OK ...>
28
+ }
29
+ - pattern-not-inside: |
30
+ {
31
+ ...
32
+ let $OK = ...;
33
+ ...
34
+ <... $OK ...>
35
+ }
36
+ - patterns:
37
+ - pattern-either:
38
+ - pattern: $V(...);
39
+ - pattern: await $V(...);
40
+ - pattern: $O.$V(...);
41
+ - pattern: await $O.$V(...);
42
+ - pattern-not-inside: const $X = ...;
43
+ - pattern-not-inside: let $X = ...;
44
+ - pattern-not-inside: $X = ...;
45
+ - pattern-not-inside: return ...;
46
+ - pattern-not-inside: if (...) ...
47
+ - pattern-not-inside: throw ...;
48
+ - pattern-not-inside: $A && $B
49
+ - pattern-not-inside: $A || $B
50
+ - pattern-not-inside: '!$A'
51
+ - pattern-not-inside: '$A ? $B : $C'
52
+ - metavariable-regex:
53
+ metavariable: $V
54
+ regex: ^(verifySignature|verifyHmac|verifyWebhookSignature|isValidSignature|isSignatureValid|validateSignature|checkSignature|compareSignatures|timingSafeEqual|safeCompare|verifyTokenSignature)$
55
+ metadata:
56
+ sec_ids: [SEC-API-101, SEC-AUTH-002]
57
+ severity_hint: CRITICAL
58
+ confidence: MEDIUM
59
+ note: >-
60
+ Finds a boolean returning check whose result is thrown away, or stored in a variable that nothing reads afterwards. The
61
+ function names are a convention guess, extend the list with the project's own verify helpers. A check that throws on
62
+ failure, such as a library constructEvent call, is not in the list.
@@ -0,0 +1,29 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Rare Phronesis Limited
3
+ # Copied from the Hullproof kit (kit/tools/hullproof/rules/webhook-parse-before-verify.yaml).
4
+ #
5
+ rules:
6
+ - id: hullproof-webhook-parse-before-verify
7
+ languages: [typescript, javascript]
8
+ severity: ERROR
9
+ message: >-
10
+ The request body is parsed as JSON before the webhook signature is verified. Read the raw
11
+ text first, verify the signature over those exact bytes, then parse.
12
+ patterns:
13
+ - pattern-either:
14
+ - pattern: |
15
+ $B = await $REQ.json();
16
+ ...
17
+ $OBJ.$M(...);
18
+ - pattern: |
19
+ $B = await $REQ.formData();
20
+ ...
21
+ $OBJ.$M(...);
22
+ - metavariable-regex:
23
+ metavariable: $M
24
+ regex: ^(constructEvent|constructEventAsync|verify|verifySignature|verifyWebhook|verifyWebhookSignature|validateEvent|unwrap|isValidSignature)$
25
+ metadata:
26
+ sec_ids: [SEC-API-101]
27
+ severity_hint: HIGH
28
+ confidence: MEDIUM
29
+ note: Heuristic. Matches a JSON body read followed by a verify style call in the same block.
@@ -0,0 +1,4 @@
1
+ {
2
+ "about": "Requirements whose subject is test material that ships in the product. A scanner hit in a test path still fails these.",
3
+ "sec_ids": []
4
+ }