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,22 @@
1
+ {
2
+ "category": "mobile",
3
+ "entries": [
4
+ {"id": "android", "signals": [
5
+ {"files": ["AndroidManifest.xml"]},
6
+ {"files": ["build.gradle", "build.gradle.kts"], "regex": "com\\.android\\.(application|library)", "confidence": "high"}]},
7
+ {"id": "capacitor", "deps": {"npm": ["@capacitor/core"]}, "signals": [{"files": ["capacitor.config.json", "capacitor.config.ts", "capacitor.config.js"]}]},
8
+ {"id": "cordova", "deps": {"npm": ["cordova", "cordova-android", "cordova-ios"]}, "signals": [{"files": ["config.xml"], "regex": "xmlns(:cdv)?=\"http://cordova\\.apache\\.org", "confidence": "high"}]},
9
+ {"id": "dotnet-maui", "signals": [{"files": ["*.csproj"], "regex": "<UseMaui>\\s*true", "confidence": "high"}]},
10
+ {"id": "expo", "deps": {"npm": ["expo"]}, "signals": [
11
+ {"files": ["app.json"], "regex": "^\\s*\"expo\"\\s*:", "confidence": "high"},
12
+ {"files": ["app.config.js", "app.config.ts"]},
13
+ {"files": ["eas.json"]}]},
14
+ {"id": "flutter", "deps": {"pub": ["flutter"]}, "imports": {"dart": ["flutter"]}, "signals": [{"files": ["pubspec.yaml"], "regex": "^\\s+sdk:\\s*flutter\\s*$", "confidence": "high"}]},
15
+ {"id": "ionic", "deps": {"npm": ["@ionic/*"]}, "signals": [{"files": ["ionic.config.json"]}]},
16
+ {"id": "ios", "imports": {"swift": ["UIKit", "SwiftUI"]}, "signals": [
17
+ {"files": ["**/*.xcodeproj/project.pbxproj"]},
18
+ {"files": ["Podfile"], "regex": "^\\s*platform\\s+:ios", "confidence": "high"},
19
+ {"files": ["Info.plist"], "regex": "UIApplicationSceneManifest|LSRequiresIPhoneOS|UILaunchStoryboardName", "confidence": "high"}]},
20
+ {"id": "react-native", "deps": {"npm": ["react-native"]}, "imports": {"js": ["react-native"]}}
21
+ ]
22
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "category": "package_managers",
3
+ "entries": [
4
+ {"id": "bun", "signals": [
5
+ {"files": ["bun.lockb", "bun.lock"], "attrs": {"lockfiles": "$file"}},
6
+ {"files": ["package.json"], "regex": "\"packageManager\"\\s*:\\s*\"bun@", "confidence": "high", "attrs": {"manifests": "$file"}}]},
7
+ {"id": "bundler", "signals": [{"files": ["Gemfile"], "attrs": {"manifests": "$file"}}, {"files": ["Gemfile.lock"], "attrs": {"lockfiles": "$file"}}]},
8
+ {"id": "cargo", "signals": [{"files": ["Cargo.toml"], "attrs": {"manifests": "$file"}}, {"files": ["Cargo.lock"], "attrs": {"lockfiles": "$file"}}]},
9
+ {"id": "carthage", "signals": [{"files": ["Cartfile"], "attrs": {"manifests": "$file"}}, {"files": ["Cartfile.resolved"], "attrs": {"lockfiles": "$file"}}]},
10
+ {"id": "cocoapods", "signals": [{"files": ["Podfile"], "attrs": {"manifests": "$file"}}, {"files": ["Podfile.lock"], "attrs": {"lockfiles": "$file"}}]},
11
+ {"id": "composer", "signals": [{"files": ["composer.json"], "attrs": {"manifests": "$file"}}, {"files": ["composer.lock"], "attrs": {"lockfiles": "$file"}}]},
12
+ {"id": "conda", "signals": [{"files": ["environment.yml", "environment.yaml"], "regex": "^dependencies:", "attrs": {"manifests": "$file"}}]},
13
+ {"id": "deno", "signals": [{"files": ["deno.json", "deno.jsonc"], "attrs": {"manifests": "$file"}}, {"files": ["deno.lock"], "attrs": {"lockfiles": "$file"}}]},
14
+ {"id": "go-modules", "signals": [{"files": ["go.mod"], "attrs": {"manifests": "$file"}}, {"files": ["go.sum"], "attrs": {"lockfiles": "$file"}}]},
15
+ {"id": "gradle", "signals": [{"files": ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"], "attrs": {"manifests": "$file"}}, {"files": ["gradle.lockfile"], "attrs": {"lockfiles": "$file"}}]},
16
+ {"id": "hex", "signals": [{"files": ["mix.exs"], "attrs": {"manifests": "$file"}}, {"files": ["mix.lock"], "attrs": {"lockfiles": "$file"}}]},
17
+ {"id": "maven", "signals": [{"files": ["pom.xml"], "attrs": {"manifests": "$file"}}]},
18
+ {"id": "npm", "signals": [
19
+ {"files": ["package-lock.json", "npm-shrinkwrap.json"], "attrs": {"lockfiles": "$file"}},
20
+ {"files": ["package.json"], "regex": "\"packageManager\"\\s*:\\s*\"npm@", "confidence": "high", "attrs": {"manifests": "$file"}}]},
21
+ {"id": "nuget", "signals": [{"files": ["*.csproj", "*.fsproj", "*.vbproj", "packages.config"], "attrs": {"manifests": "$file"}}, {"files": ["packages.lock.json"], "attrs": {"lockfiles": "$file"}}]},
22
+ {"id": "pdm", "signals": [{"files": ["pdm.lock"], "attrs": {"lockfiles": "$file"}}, {"files": ["pyproject.toml"], "regex": "^\\[tool\\.pdm\\b", "confidence": "high", "attrs": {"manifests": "$file"}}]},
23
+ {"id": "pip", "signals": [{"files": ["requirements*.txt", "**/requirements/*.txt", "requirements*.in"], "attrs": {"manifests": "$file"}}]},
24
+ {"id": "pipenv", "signals": [{"files": ["Pipfile"], "attrs": {"manifests": "$file"}}, {"files": ["Pipfile.lock"], "attrs": {"lockfiles": "$file"}}]},
25
+ {"id": "pnpm", "signals": [
26
+ {"files": ["pnpm-lock.yaml"], "attrs": {"lockfiles": "$file"}},
27
+ {"files": ["pnpm-workspace.yaml"], "attrs": {"manifests": "$file"}},
28
+ {"files": ["package.json"], "regex": "\"packageManager\"\\s*:\\s*\"pnpm@", "confidence": "high", "attrs": {"manifests": "$file"}}]},
29
+ {"id": "poetry", "signals": [{"files": ["poetry.lock"], "attrs": {"lockfiles": "$file"}}, {"files": ["pyproject.toml"], "regex": "^\\[tool\\.poetry\\]", "confidence": "high", "attrs": {"manifests": "$file"}}]},
30
+ {"id": "pub", "signals": [{"files": ["pubspec.yaml"], "attrs": {"manifests": "$file"}}, {"files": ["pubspec.lock"], "attrs": {"lockfiles": "$file"}}]},
31
+ {"id": "sbt", "signals": [{"files": ["build.sbt"], "attrs": {"manifests": "$file"}}]},
32
+ {"id": "swift-package-manager", "signals": [{"files": ["Package.swift"], "attrs": {"manifests": "$file"}}, {"files": ["Package.resolved"], "attrs": {"lockfiles": "$file"}}]},
33
+ {"id": "uv", "signals": [{"files": ["uv.lock"], "attrs": {"lockfiles": "$file"}}, {"files": ["pyproject.toml"], "regex": "^\\[tool\\.uv\\b", "confidence": "high", "attrs": {"manifests": "$file"}}]},
34
+ {"id": "yarn", "signals": [
35
+ {"files": ["yarn.lock"], "attrs": {"lockfiles": "$file"}},
36
+ {"files": [".yarnrc.yml", ".yarnrc"], "attrs": {"manifests": "$file"}},
37
+ {"files": ["package.json"], "regex": "\"packageManager\"\\s*:\\s*\"yarn@", "confidence": "high", "attrs": {"manifests": "$file"}}]}
38
+ ]
39
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "category": "payments",
3
+ "entries": [
4
+ {"id": "adyen", "deps": {"npm": ["@adyen/*"], "pypi": ["adyen"]}},
5
+ {"id": "app-store-billing", "deps": {"npm": ["react-native-iap", "expo-in-app-purchases"], "pub": ["in_app_purchase"], "maven": ["com.android.billingclient*"]}, "imports": {"swift": ["StoreKit"]}},
6
+ {"id": "braintree", "deps": {"npm": ["braintree", "braintree-web"], "pypi": ["braintree"], "rubygems": ["braintree"]}},
7
+ {"id": "flutterwave", "deps": {"npm": ["flutterwave-node-v3", "flutterwave-react-v3"], "pub": ["flutterwave_standard"]}},
8
+ {"id": "lemon-squeezy", "deps": {"npm": ["@lemonsqueezy/lemonsqueezy.js"]}},
9
+ {"id": "mollie", "deps": {"npm": ["@mollie/api-client"], "pypi": ["mollie-api-python"], "composer": ["mollie/mollie-api-php"]}},
10
+ {"id": "paddle", "deps": {"npm": ["@paddle/*"], "pypi": ["paddle-python-sdk"]}},
11
+ {"id": "paypal", "deps": {"npm": ["@paypal/*"], "pypi": ["paypalrestsdk", "paypal-server-sdk"]}},
12
+ {"id": "paystack", "deps": {"npm": ["paystack", "@paystack/*", "react-paystack", "paystack-api"], "pub": ["flutter_paystack"]}, "signals": [{"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs", "*.py", "*.php", "*.rb", "*.go", "*.dart"], "regex": "api\\.paystack\\.co"}]},
13
+ {"id": "polar", "deps": {"npm": ["@polar-sh/*"], "pypi": ["polar-sdk"]}},
14
+ {"id": "razorpay", "deps": {"npm": ["razorpay"], "pypi": ["razorpay"]}},
15
+ {"id": "revenuecat", "deps": {"npm": ["react-native-purchases"], "pub": ["purchases_flutter"]}},
16
+ {"id": "square", "deps": {"npm": ["square"], "pypi": ["squareup"]}},
17
+ {"id": "stripe", "deps": {"npm": ["stripe", "@stripe/*"], "pypi": ["stripe"], "go": ["github.com/stripe/stripe-go*"], "rubygems": ["stripe"], "composer": ["stripe/stripe-php"], "maven": ["com.stripe:stripe-java"], "nuget": ["Stripe.net"], "pub": ["flutter_stripe"]}, "imports": {"js": ["stripe", "@stripe/stripe-js"], "python": ["stripe"]}}
18
+ ]
19
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "category": "runtimes",
3
+ "entries": [
4
+ {"id": "bun", "signals": [{"files": ["bun.lockb", "bun.lock", "bunfig.toml"]}, {"files": ["package.json"], "regex": "\"packageManager\"\\s*:\\s*\"bun@", "confidence": "high"}]},
5
+ {"id": "deno", "signals": [{"files": ["deno.json", "deno.jsonc", "deno.lock"]}]},
6
+ {"id": "dotnet", "signals": [{"files": ["*.csproj", "*.fsproj", "*.vbproj", "*.sln", "global.json"]}]},
7
+ {"id": "dart", "signals": [{"files": ["pubspec.yaml"]}]},
8
+ {"id": "elixir", "signals": [{"files": ["mix.exs"]}]},
9
+ {"id": "go", "signals": [{"files": ["go.mod"]}]},
10
+ {"id": "jvm", "signals": [{"files": ["pom.xml", "build.gradle", "build.gradle.kts", "build.sbt"]}]},
11
+ {"id": "node", "signals": [{"files": ["package.json", ".nvmrc", ".node-version"]}]},
12
+ {"id": "php", "signals": [{"files": ["composer.json"]}]},
13
+ {"id": "python", "signals": [{"files": ["pyproject.toml", "requirements*.txt", "setup.py", "setup.cfg", "Pipfile", ".python-version"]}]},
14
+ {"id": "ruby", "signals": [{"files": ["Gemfile", ".ruby-version"]}]},
15
+ {"id": "rust", "signals": [{"files": ["Cargo.toml"]}]}
16
+ ]
17
+ }
@@ -0,0 +1,46 @@
1
+ // check.authz.admin-role-check: every server handler under an admin path checks an admin role on the server.
2
+ // A session check alone (any signed in user) is not a role check. Middleware or a shared guard that checks the role for
3
+ // the whole admin path is not followed in Phase 1; such a project sees this FAIL and the report says how to confirm it.
4
+ import { fileURLToPath } from 'node:url';
5
+ import { byFileLine, lineOf } from '../context.js';
6
+ import { handlerFiles } from './shared.js';
7
+ const ADMIN_PATH = /(^|\/)(admin|backoffice|staff|superuser)(\/|\.|$)/i;
8
+ const ROLE_CHECK = new RegExp([
9
+ String.raw `\brole\s*(?:===?|!==?|==)\s*["'\x60](?:admin|owner|superadmin|staff)`,
10
+ String.raw `["'\x60](?:admin|owner|superadmin|staff)["'\x60]\s*(?:===?|!==?)\s*[\w.?]*role`,
11
+ String.raw `\b(?:is_?admin|isAdmin|requireAdmin|require_admin|assertAdmin|ensureAdmin|adminOnly|admin_required|requireRole|require_role|checkRole|withRole|hasRole|has_role|hasPermission|has_perm|is_staff|is_superuser|authorize)\b`,
12
+ String.raw `@(?:PreAuthorize|Secured|RolesAllowed|Roles|staff_member_required|user_passes_test|permission_required)`,
13
+ String.raw `\bcurrent_user\.admin\?|\buser\.admin\b|roles?\.includes\(\s*["']admin`,
14
+ ].join('|'));
15
+ const HANDLER = /export\s+(?:async\s+)?(?:function\s+|const\s+)(GET|POST|PUT|PATCH|DELETE|default|handler)\b|@(?:app|router|api_view|\w+)\.(?:get|post|put|patch|delete|route)\(|\bdef\s+(?:get|post|put|patch|delete|index|show|create|update|destroy)\b|\b(?:router|app)\.(?:get|post|put|patch|delete)\(/g;
16
+ export function run(ctx) {
17
+ const files = handlerFiles(ctx).filter((f) => ADMIN_PATH.test(f));
18
+ if (files.length === 0)
19
+ return { value: 'not_applicable', reason: 'no server handler under an admin path', evidence: [], absence: null };
20
+ const bad = [];
21
+ for (const file of files) {
22
+ const text = ctx.text(file);
23
+ if (ROLE_CHECK.test(text))
24
+ continue;
25
+ const handlers = [...text.matchAll(HANDLER)];
26
+ const lines = handlers.length ? handlers.map((h) => lineOf(text, h.index)) : [1];
27
+ for (const line of lines)
28
+ bad.push({ file, line, note: 'admin handler with no server side admin role check' });
29
+ }
30
+ return {
31
+ value: bad.length === 0,
32
+ evidence: bad.sort(byFileLine),
33
+ absence: { pattern: ROLE_CHECK.source, files_searched: files.length, file_globs: ['handler files under an admin path'], matches: files.length - new Set(bad.map((b) => b.file)).size },
34
+ };
35
+ }
36
+ export const check = {
37
+ id: 'check.authz.admin-role-check',
38
+ sec_ids: ['SEC-AUTHZ-020'],
39
+ applies_when: {},
40
+ op: 'code',
41
+ on_true: 'NOT ASSESSED: NEEDS DYNAMIC TEST',
42
+ on_false: 'FAIL',
43
+ fixtures: { pass: 'test/fixtures/checks/admin-role-check/pass', fail: 'test/fixtures/checks/admin-role-check/fail' },
44
+ source: fileURLToPath(import.meta.url),
45
+ run,
46
+ };
@@ -0,0 +1,91 @@
1
+ // Two checks from one detector: a record read by an id that came from the request (or from a model's tool arguments)
2
+ // must also be filtered by its owner. check.authz.owner-filter-on-id-read covers request handlers (SEC-AUTHZ-003);
3
+ // check.ai.tool-owner-filter covers functions handed to a model as tools (SEC-AI-021).
4
+ // Detected read forms: SQL text (any driver), Prisma and Drizzle style where objects, Supabase .eq('id'), Mongoose
5
+ // findById, Django .get(pk=) and get_object_or_404, Rails .find(params[:id]), Laravel ::find / findOrFail.
6
+ // It reads code, so it can FAIL those runtime requirements for the part a file settles and never PASS them.
7
+ import { fileURLToPath } from 'node:url';
8
+ import { byFileLine, lineOf } from '../context.js';
9
+ import { handlerFiles, NOT_HANDLER } from './shared.js';
10
+ const READS = [
11
+ { re: /\bSELECT\b[^;"'`]*?\bWHERE\s+(?:\w+\.)?(?:id|pk|uuid)\s*=\s*(?:\?|\$\d+|:\w+|%s|@\w+|\$\{)/gi, what: 'SQL read by id' },
12
+ { re: /\.(?:findUnique|findFirst|findOne|findOneBy|findOneOrFail)\(\s*\{\s*where:\s*\{\s*id\s*[:,}]/g, what: 'ORM read by id' },
13
+ { re: /\.(?:findById|findByPk|findOneById)\(/g, what: 'ORM read by id' },
14
+ { re: /\.eq\(\s*["'](?:id|uuid)["']\s*,/g, what: 'Supabase read by id' },
15
+ { re: /\.objects\.get\(\s*(?:id|pk)\s*=|get_object_or_404\(\s*\w+\s*,\s*(?:id|pk)\s*=/g, what: 'Django read by id' },
16
+ { re: /\.find\(\s*params\[:id\]\s*\)|::(?:find|findOrFail)\(\s*\$(?:id|request)/g, what: 'Rails or Laravel read by id' },
17
+ ];
18
+ // Words that show the read is scoped to the caller.
19
+ const OWNER = /owner|user_?id|userId|tenant|account_?id|accountId|org_?id|orgId|organization|workspace|created_by|createdBy|author|auth\.uid|current_?user|request\.user|session\.user|\.user\.id|ctx\.user|scope|policy/i;
20
+ // Words that show the id came from the request or from model arguments.
21
+ const INPUT = /params|args|arguments|req\.|request\.|query|body|input|searchParams|\$_(GET|POST|REQUEST)|kwargs/;
22
+ const WRITE = /\b(UPDATE|DELETE|INSERT)\b/i;
23
+ // The statement around a match: from the match to the first semicolon or blank line, at most 6 lines.
24
+ function statement(text, at) {
25
+ const lines = text.slice(at).split('\n').slice(0, 6);
26
+ const out = [];
27
+ for (const l of lines) {
28
+ if (out.length && l.trim() === '')
29
+ break;
30
+ out.push(l);
31
+ if (/;\s*$/.test(l))
32
+ break;
33
+ }
34
+ return out.join('\n');
35
+ }
36
+ function scan(files, ctx) {
37
+ const bad = [];
38
+ for (const file of files) {
39
+ const text = ctx.text(file);
40
+ for (const { re, what } of READS) {
41
+ re.lastIndex = 0;
42
+ for (let m = re.exec(text); m; m = re.exec(text)) {
43
+ // For SQL, judge only the WHERE clause onward: an owner column in the select list filters nothing.
44
+ const where = m[0].search(/\bWHERE\b/i);
45
+ const stmt = statement(text, m.index + Math.max(0, where));
46
+ if (WRITE.test(m[0]) || OWNER.test(stmt) || !INPUT.test(stmt))
47
+ continue;
48
+ bad.push({ file, line: lineOf(text, m.index), note: `${what} with an id from the request and no owner filter` });
49
+ }
50
+ }
51
+ }
52
+ return bad.sort(byFileLine);
53
+ }
54
+ const AI_TOOL_GLOBS = ['**/{ai,llm,agent,agents,mcp,tools}/**/*.{ts,js,mjs,py}', '**/*tool*.{ts,js,mjs,py}'];
55
+ const aiToolFiles = (ctx) => ctx.select(AI_TOOL_GLOBS, NOT_HANDLER).filter((f) => /\btools?\b|tool_call|function_call|bind_tools|@tool\b/.test(ctx.text(f)));
56
+ function outcome(files, ctx, none) {
57
+ if (files.length === 0)
58
+ return { value: 'not_applicable', reason: none, evidence: [], absence: null };
59
+ const bad = scan(files, ctx);
60
+ return {
61
+ value: bad.length === 0,
62
+ evidence: bad,
63
+ absence: { pattern: 'reads by id (SQL, ORM, Supabase, Django, Rails, Laravel) with no owner, user or tenant term in the statement', files_searched: files.length, file_globs: [], matches: bad.length },
64
+ };
65
+ }
66
+ const source = fileURLToPath(import.meta.url);
67
+ export const ownerFilter = {
68
+ id: 'check.authz.owner-filter-on-id-read',
69
+ sec_ids: ['SEC-AUTHZ-003'],
70
+ applies_when: {},
71
+ op: 'code',
72
+ on_true: 'NOT ASSESSED: NEEDS DYNAMIC TEST',
73
+ on_false: 'FAIL',
74
+ fixtures: { pass: 'test/fixtures/checks/owner-filter-on-id-read/pass', fail: 'test/fixtures/checks/owner-filter-on-id-read/fail' },
75
+ source,
76
+ run: (ctx) => {
77
+ const ai = new Set(aiToolFiles(ctx));
78
+ return outcome(handlerFiles(ctx).filter((f) => !ai.has(f)), ctx, 'no request handler found');
79
+ },
80
+ };
81
+ export const toolOwnerFilter = {
82
+ id: 'check.ai.tool-owner-filter',
83
+ sec_ids: ['SEC-AI-021'],
84
+ applies_when: {},
85
+ op: 'code',
86
+ on_true: 'NOT ASSESSED: NEEDS DYNAMIC TEST',
87
+ on_false: 'FAIL',
88
+ fixtures: { pass: 'test/fixtures/checks/tool-owner-filter/pass', fail: 'test/fixtures/checks/tool-owner-filter/fail' },
89
+ source,
90
+ run: (ctx) => outcome(aiToolFiles(ctx), ctx, 'no file that defines tools for a model was found'),
91
+ };
@@ -0,0 +1,84 @@
1
+ // Server side request handlers across common stacks. Stack specific globs are marked by the framework they serve.
2
+ export const HANDLER_GLOBS = [
3
+ '**/app/**/route.{ts,js,mjs,tsx,jsx}', // Next.js App Router
4
+ '**/pages/api/**/*.{ts,js,mjs}', // Next.js Pages Router
5
+ '**/app/actions/**/*.{ts,js}', // Next.js Server Actions folder
6
+ '**/api/**/*.{ts,js,mjs,py,go,php,rb}', // Vercel and Netlify functions, generic api folders
7
+ '**/routes/**/*.{ts,js,mjs,py,php,rb}', // Express, Remix, SvelteKit, FastAPI routers, Laravel routes
8
+ '**/routers/**/*.py', '**/views.py', '**/views/**/*.py', '**/app.py', '**/main.py', // Django, Flask, FastAPI
9
+ '**/controllers/**/*.{ts,js,rb,php}', 'app/Http/Controllers/**/*.php', // Rails, Laravel, Nest, Adonis
10
+ '**/*Controller.{java,kt,cs}', // Spring, ASP.NET
11
+ '**/handlers/**/*.{go,ts,js}', '**/*handler*.go', // Go
12
+ 'supabase/functions/**/*.ts', 'netlify/functions/**/*.{ts,js}', 'functions/**/*.{ts,js,py}', // edge and cloud functions
13
+ '**/server/**/*.{ts,js}', '**/hooks.server.{ts,js}', '**/+server.{ts,js}', '**/+page.server.{ts,js}',
14
+ ];
15
+ export const NOT_HANDLER = ['**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/test/**', '**/tests/**', '**/*.d.ts'];
16
+ // Handler files, without browser only modules ("use client").
17
+ export function handlerFiles(ctx) {
18
+ return ctx.select(HANDLER_GLOBS, NOT_HANDLER).filter((f) => !/^\s*["']use client["']/.test(ctx.text(f)));
19
+ }
20
+ // The index just past the bracket that closes the one at `open`, or -1. Skips string literals and comments, roughly.
21
+ export function closeOf(text, open) {
22
+ const pair = { '(': ')', '{': '}', '[': ']' };
23
+ const stack = [];
24
+ for (let i = open; i < text.length; i++) {
25
+ const c = text[i];
26
+ if (c === '"' || c === "'" || c === '`') {
27
+ for (i++; i < text.length && text[i] !== c; i++)
28
+ if (text[i] === '\\')
29
+ i++;
30
+ continue;
31
+ }
32
+ if (c === '/' && text[i + 1] === '/') {
33
+ while (i < text.length && text[i] !== '\n')
34
+ i++;
35
+ continue;
36
+ }
37
+ if (c === '/' && text[i + 1] === '*') {
38
+ const e = text.indexOf('*/', i + 2);
39
+ i = e < 0 ? text.length : e + 1;
40
+ continue;
41
+ }
42
+ if (pair[c])
43
+ stack.push(pair[c]);
44
+ else if (c === stack[stack.length - 1]) {
45
+ stack.pop();
46
+ if (stack.length === 0)
47
+ return i + 1;
48
+ }
49
+ }
50
+ return -1;
51
+ }
52
+ // The body of the `if` whose keyword starts at `at`: a braced block, an indented Python block, or the rest of the statement.
53
+ export function ifBody(text, at) {
54
+ const py = /^if\s+[^\n(]*:\s*(#.*)?$/m.exec(text.slice(at, text.indexOf('\n', at) < 0 ? text.length : text.indexOf('\n', at)));
55
+ if (py) {
56
+ const lineEnd = text.indexOf('\n', at);
57
+ const lineStart = text.lastIndexOf('\n', at) + 1;
58
+ const indent = /^\s*/.exec(text.slice(lineStart))[0].length;
59
+ const rest = text.slice(lineEnd + 1).split('\n');
60
+ const body = [];
61
+ for (const l of rest) {
62
+ if (l.trim() === '')
63
+ continue;
64
+ if (/^\s*/.exec(l)[0].length <= indent)
65
+ break;
66
+ body.push(l);
67
+ }
68
+ return text.slice(at, lineEnd) + '\n' + body.join('\n');
69
+ }
70
+ const paren = text.indexOf('(', at);
71
+ const condEnd = paren < 0 ? -1 : closeOf(text, paren);
72
+ if (condEnd < 0)
73
+ return text.slice(at, at + 200);
74
+ const after = text.slice(condEnd).search(/\S/);
75
+ const start = condEnd + (after < 0 ? 0 : after);
76
+ if (text[start] === '{') {
77
+ const e = closeOf(text, start);
78
+ return text.slice(at, e < 0 ? start + 400 : e);
79
+ }
80
+ const end = text.slice(start).search(/[;\n]/);
81
+ return text.slice(at, end < 0 ? text.length : start + end + 1);
82
+ }
83
+ // A statement that stops the request: return, throw, raise, abort, or an explicit 4xx or 5xx response.
84
+ export const EXITS = /\b(return|throw|raise|abort|panic|exit|die)\b|\bstatus\(\s*[45]\d\d|\bhttp\.Error\b|res\.sendStatus\(\s*[45]/;
@@ -0,0 +1,77 @@
1
+ // check.api.webhook-verified-before-use: a webhook handler checks the sender's signature, and a failed check stops the
2
+ // request before the event is used. Works on any stack whose handlers are found by handlerFiles() (JS, TS, Python, Go,
3
+ // PHP, Ruby). It reads code, so it can FAIL SEC-API-101 for the part a file settles and never PASS it.
4
+ import { fileURLToPath } from 'node:url';
5
+ import { byFileLine, lineOf } from '../context.js';
6
+ import { EXITS, handlerFiles, ifBody } from './shared.js';
7
+ // A file is a webhook handler when its path says webhook or it reads a provider signature header.
8
+ const WEBHOOK_PATH = /webhook/i;
9
+ const SIGNATURE_HEADER = /\b[xX]-[a-zA-Z-]*[sS]ignature\b|stripe-signature|paystack-signature|svix-signature|webhook-signature|HTTP_X_[A-Z_]*SIGNATURE/;
10
+ // Calls that check a signature and return a result the caller must act on.
11
+ const VERIFY_CALL = /\b((?:\w+\.)?(?:verify\w*|validate\w*[sS]ignature\w*|check\w*[sS]ignature\w*|is\w*[vV]alid\w*[sS]ignature\w*|compare_digest|hash_equals|secure_compare|timingSafeEqual|hmac\.Equal|ValidatePayload))\s*\(/g;
12
+ // Calls documented by their providers to throw (or raise) on a bad signature, so code after them runs only when valid.
13
+ const THROWING = /constructEvent|construct_event|webhooks\.verify|wh\.verify|Webhook\.construct|unwrap\(|constructEventAsync|validateRequest/;
14
+ export function scanFile(file, text) {
15
+ const bad = [];
16
+ let calls = 0;
17
+ VERIFY_CALL.lastIndex = 0;
18
+ for (let m = VERIFY_CALL.exec(text); m; m = VERIFY_CALL.exec(text)) {
19
+ if (/function\s+$|def\s+$|func\s+(\([^)]*\)\s*)?$/.test(text.slice(Math.max(0, m.index - 30), m.index)))
20
+ continue; // a definition, not a call
21
+ calls++;
22
+ const lineStart = text.lastIndexOf('\n', m.index) + 1;
23
+ const lineText = text.slice(lineStart, text.indexOf('\n', m.index) < 0 ? text.length : text.indexOf('\n', m.index));
24
+ if (THROWING.test(m[1]) || THROWING.test(lineText))
25
+ continue;
26
+ // The call sits in an if condition: `if (!verify(...)) return ...` or `if not verify(...): raise ...`.
27
+ const ifAt = /\bif\b/.exec(lineText);
28
+ if (ifAt && lineStart + ifAt.index < m.index) {
29
+ const body = ifBody(text, lineStart + ifAt.index);
30
+ if (!EXITS.test(body.slice(body.indexOf(m[0]) + m[0].length)))
31
+ bad.push({ file, line: lineOf(text, m.index), note: 'a failed signature check does not stop the request' });
32
+ continue;
33
+ }
34
+ // The result is stored: `const ok = verify(...)`, `ok := verify(...)`, `ok = verify(...)`.
35
+ const assigned = /(?:const|let|var|val)?\s*([A-Za-z_$][\w$]*)\s*(?::=|=)\s*(?:await\s+)?[\w.$]*$/.exec(text.slice(lineStart, m.index));
36
+ if (!assigned)
37
+ continue; // a bare call: its result is not used as a gate, and most such helpers throw
38
+ const name = assigned[1];
39
+ const after = text.slice(m.index);
40
+ const guard = new RegExp(`\\bif\\s*\\(?\\s*(?:!\\s*${name}\\b|not\\s+${name}\\b|${name}\\s*(?:===?|is)\\s*(?:false|False|nil|None)|${name}\\s*!==?\\s*true)`, 'g');
41
+ const guards = [...after.matchAll(guard)];
42
+ if (guards.length === 0) {
43
+ bad.push({ file, line: lineOf(text, m.index), note: `the result ${name} of the signature check is never used to stop the request` });
44
+ continue;
45
+ }
46
+ const stops = guards.some((g) => EXITS.test(ifBody(text, m.index + g.index)));
47
+ if (!stops)
48
+ bad.push({ file, line: lineOf(text, m.index + guards[0].index), note: `when ${name} is false the handler carries on and uses the event` });
49
+ }
50
+ if (calls === 0 && !THROWING.test(text)) {
51
+ const handler = /export\s+(?:async\s+)?(?:function|const)\s+(?:POST|PUT|handler|default)\b|@(?:app|router)\.post|\bdef\s+\w*webhook\w*|func\s+\w*[Ww]ebhook\w*/.exec(text);
52
+ bad.push({ file, line: handler ? lineOf(text, handler.index) : 1, note: 'no call that checks the sender signature was found in this webhook handler' });
53
+ }
54
+ return bad;
55
+ }
56
+ export function run(ctx) {
57
+ const files = handlerFiles(ctx).filter((f) => WEBHOOK_PATH.test(f) || SIGNATURE_HEADER.test(ctx.text(f)));
58
+ if (files.length === 0)
59
+ return { value: 'not_applicable', reason: 'no webhook handler found', evidence: [], absence: null };
60
+ const bad = files.flatMap((f) => scanFile(f, ctx.text(f))).sort(byFileLine);
61
+ return {
62
+ value: bad.length === 0,
63
+ evidence: bad.length ? bad : files.map((file) => ({ file, line: 1, note: 'signature check found and a failure stops the request' })),
64
+ absence: { pattern: `${VERIFY_CALL.source} followed by a guard that returns or throws`, files_searched: files.length, file_globs: ['handler files whose path says webhook or that read a signature header'], matches: files.length - new Set(bad.map((b) => b.file)).size },
65
+ };
66
+ }
67
+ export const check = {
68
+ id: 'check.api.webhook-verified-before-use',
69
+ sec_ids: ['SEC-API-101', 'SEC-API-126'],
70
+ applies_when: {},
71
+ op: 'code',
72
+ on_true: 'NOT ASSESSED: NEEDS DYNAMIC TEST',
73
+ on_false: 'FAIL',
74
+ fixtures: { pass: 'test/fixtures/checks/webhook-verified-before-use/pass', fail: 'test/fixtures/checks/webhook-verified-before-use/fail' },
75
+ source: fileURLToPath(import.meta.url),
76
+ run,
77
+ };
@@ -0,0 +1,64 @@
1
+ // The read only view of a repository that every check runs against: the file list, file text, stack.json and scan hits.
2
+ import { closeSync, openSync, readSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { LIMITS } from '../discover/discover.js';
5
+ import { walk } from '../discover/walk.js';
6
+ import { cmp, globToRegExp } from '../util.js';
7
+ import { readTracked } from '../scanners/common.js';
8
+ // Folders no check searches: engine output, the kit's own files and the project's security records. The kit folders hold
9
+ // text that names almost every term a check looks for (STANDARD.md, "What the searches leave out").
10
+ export const LEFT_OUT = ['.hullproof/', 'docs/hullproof/', 'tools/hullproof/', '.claude/', 'editors/', 'prompts/', 'docs/security/'];
11
+ function readHead(abs, max) {
12
+ const fd = openSync(abs, 'r');
13
+ try {
14
+ const buf = Buffer.alloc(max);
15
+ const n = readSync(fd, buf, 0, max, 0);
16
+ return buf.subarray(0, n).toString('utf8');
17
+ }
18
+ finally {
19
+ closeSync(fd);
20
+ }
21
+ }
22
+ export function makeContext(repo, stack, hits = [], kitDir = null) {
23
+ const root = resolve(repo);
24
+ const files = walk(root, LIMITS.maxFiles).files.filter((f) => !LEFT_OUT.some((p) => f.startsWith(p)));
25
+ const cache = new Map();
26
+ const text = (f) => {
27
+ let t = cache.get(f);
28
+ if (t === undefined) {
29
+ try {
30
+ t = readHead(join(root, f), LIMITS.maxBytesPerFile);
31
+ }
32
+ catch {
33
+ t = '';
34
+ }
35
+ cache.set(f, t);
36
+ }
37
+ return t;
38
+ };
39
+ const select = (globs, exclude = []) => {
40
+ const inc = globs.map(globToRegExp);
41
+ const exc = exclude.map(globToRegExp);
42
+ return files.filter((f) => inc.some((r) => r.test(f)) && !exc.some((r) => r.test(f)));
43
+ };
44
+ let trackedList;
45
+ const tracked = () => {
46
+ if (trackedList === undefined) {
47
+ const t = readTracked(root);
48
+ trackedList = t ? [...t].sort(cmp) : null;
49
+ }
50
+ return trackedList;
51
+ };
52
+ return { repo: root, files, stack, hits, kitDir, text, select, tracked };
53
+ }
54
+ // 1 based line of a character index.
55
+ export function lineOf(text, index) {
56
+ let n = 1;
57
+ for (let i = 0; i < index && i < text.length; i++)
58
+ if (text.charCodeAt(i) === 10)
59
+ n++;
60
+ return n;
61
+ }
62
+ export const byFileLine = (a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : (a.line ?? 0) - (b.line ?? 0) || (a.note < b.note ? -1 : a.note > b.note ? 1 : 0);
63
+ // The most evidence entries one check result keeps. The count of all matches is kept in absence_search.
64
+ export const MAX_EVIDENCE = 25;
@@ -0,0 +1,74 @@
1
+ // Checks that read the configuration maps: Supabase tables (grants and policies) and Firebase rules. Start as leads.
2
+ import { fileURLToPath } from 'node:url';
3
+ import { PRIVILEGED } from '../../map/vocab.js';
4
+ import { mapCheck } from './shared.js';
5
+ const source = fileURLToPath(import.meta.url);
6
+ const NATIVE_FIXTURES = { pass: 'test/fixtures/map-native/pass', fail: 'test/fixtures/map-native/fail' };
7
+ const CLIENT_ROLES = new Set(['anon', 'authenticated', 'public']);
8
+ const tables = (m) => m.config.supabase?.tables ?? [];
9
+ const alwaysTrue = (e) => e !== null && /^\(?\s*true\s*\)?$/i.test(e.trim());
10
+ const WRITE_PRIVS = new Set(['insert', 'update', 'delete', 'all', 'truncate']);
11
+ export const supabaseOpenTable = mapCheck({
12
+ id: 'check.map.supabase-open-table', sec_ids: ['SEC-DB-001', 'SEC-DB-003'], name: 'supabase-open-table', source,
13
+ pattern: 'a table granted to anon without row level security, or with a policy for anon or public that is always true',
14
+ none: 'the code map has no Supabase table',
15
+ candidates: (m) => tables(m).length,
16
+ fails: (m) => tables(m).flatMap((t) => {
17
+ const out = [];
18
+ const anon = t.grants.filter((g) => g.role === 'anon' || g.role === 'public');
19
+ if (t.rls_enabled !== true && anon.length)
20
+ out.push({ file: t.file, line: anon[0].line, note: `${t.name} has no row level security and ${anon[0].role} holds ${[...new Set(anon.flatMap((g) => g.privileges))].join(', ')}` });
21
+ for (const p of t.policies) {
22
+ if (p.roles.some((r) => r === 'anon' || r === 'public') && (alwaysTrue(p.using) || (p.using === null && alwaysTrue(p.check)) || alwaysTrue(p.check)))
23
+ out.push({ file: t.file, line: p.line, note: `${t.name} policy ${p.name} lets ${p.roles.join(', ')} ${p.op} with an expression that is always true` });
24
+ }
25
+ return out;
26
+ }),
27
+ });
28
+ export const supabasePrivilegedColumnGrant = mapCheck({
29
+ id: 'check.map.supabase-privileged-column-grant', sec_ids: ['SEC-DB-033'], name: 'supabase-privileged-column-grant', source,
30
+ pattern: 'a client role holding insert or update on a table that has a role, plan, admin or similar column, without a column list that leaves it out',
31
+ none: 'the code map has no Supabase table with a role, plan or admin column',
32
+ candidates: (m) => tables(m).filter((t) => t.columns.some((c) => PRIVILEGED.test(c))).length,
33
+ fails: (m) => tables(m).flatMap((t) => {
34
+ const priv = t.columns.filter((c) => PRIVILEGED.test(c) && !/^(?:owner_?id|org_?id|tenant_?id)$/i.test(c));
35
+ if (!priv.length)
36
+ return [];
37
+ const lines = new Map();
38
+ for (const g of t.grants.filter((x) => CLIENT_ROLES.has(x.role) && x.privileges.some((p) => p === 'update' || p === 'insert' || p === 'all'))) {
39
+ const k = `${g.role}|${g.line}|${(g.columns ?? ['*']).join(',')}`;
40
+ const cur = lines.get(k) ?? { role: g.role, privileges: new Set(), columns: g.columns, line: g.line };
41
+ for (const p of g.privileges)
42
+ cur.privileges.add(p);
43
+ lines.set(k, cur);
44
+ }
45
+ return [...lines.values()].flatMap((g) => {
46
+ const reach = g.columns === null ? priv : priv.filter((c) => g.columns.includes(c));
47
+ return reach.length ? [{ file: t.file, line: g.line, note: `${t.name}: ${g.role} can ${[...g.privileges].sort().join(', ')} ${g.columns === null ? 'every column' : 'the listed columns'} including ${reach.join(', ')}` }] : [];
48
+ });
49
+ }),
50
+ });
51
+ export const supabasePolicyTrustsClaims = mapCheck({
52
+ id: 'check.map.supabase-policy-trusts-claims', sec_ids: ['SEC-DB-006'], name: 'supabase-policy-trusts-claims', source,
53
+ pattern: 'a policy expression that reads user_metadata or raw_user_meta_data',
54
+ none: 'the code map has no Supabase policy',
55
+ candidates: (m) => tables(m).reduce((n, t) => n + t.policies.length, 0),
56
+ fails: (m) => tables(m).flatMap((t) => t.policies.filter((p) => /user_metadata|raw_user_meta_data/i.test(`${p.using ?? ''} ${p.check ?? ''}`)).map((p) => ({ file: t.file, line: p.line, note: `${t.name} policy ${p.name} trusts user_metadata, which the signed in user can edit` }))),
57
+ });
58
+ export const firebaseAuthOnlyRule = mapCheck({
59
+ id: 'check.map.firebase-auth-only-rule', sec_ids: ['SEC-AUTHZ-010'], name: 'firebase-auth-only-rule', source,
60
+ pattern: 'a rule on a path with an owner wildcard whose only condition is that the caller is signed in',
61
+ none: 'the code map has no Firebase rule on a path with an owner wildcard',
62
+ candidates: (m) => (m.config.firebase?.rules ?? []).filter((r) => r.owner_wildcard).length,
63
+ fails: (m) => (m.config.firebase?.rules ?? []).filter((r) => r.owner_wildcard && r.uses_auth && !r.owner_bound && !r.always_true).map((r) => ({
64
+ file: r.file, line: r.line, note: `${r.service} ${r.path} allows ${r.ops.join(', ')} to any signed in user, not to the owner named in the path`,
65
+ })),
66
+ });
67
+ export const mobileSecretInConfig = mapCheck({
68
+ id: 'check.map.mobile-secret-in-config', sec_ids: ['SEC-MOBILE-001'], name: 'mobile-secret-in-config', source, fixtures: NATIVE_FIXTURES,
69
+ pattern: 'a value written in AndroidManifest.xml, an Android strings file or an iOS Info.plist under a name that says secret, password or private key',
70
+ none: 'the code map has no Android or iOS configuration file',
71
+ candidates: (m) => (m.config.android?.manifests.length ?? 0) + (m.config.ios?.plists.length ?? 0),
72
+ fails: (m) => m.config.secrets.map((x) => ({ file: x.file, line: x.line, note: `${x.where}: the name ${x.key} holds a value written into the file, and this file ships inside the app` })),
73
+ });
74
+ export const CONFIG_CHECKS = [supabaseOpenTable, supabasePrivilegedColumnGrant, supabasePolicyTrustsClaims, firebaseAuthOnlyRule, mobileSecretInConfig];