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,253 @@
1
+ import { cmp } from '../util.js';
2
+ import { ENGINE_VERSION } from '../version.js';
3
+ import { firebaseMap, supabaseMap } from './config.js';
4
+ import { joinPath } from './entry.js';
5
+ import { extractExpress } from './js-express.js';
6
+ import { extractAiTools, extractJobs, extractNest } from './js-misc.js';
7
+ import { extractNext } from './js-next.js';
8
+ import { extractExpo } from './expo.js';
9
+ import { extractGoAll, goFiles } from './go.js';
10
+ import { extractLaravelAll, laravelFiles } from './laravel.js';
11
+ import { FnIndex } from './light-index.js';
12
+ import { extractRailsAll, railsFiles } from './rails.js';
13
+ import { extractSpring, springFiles } from './spring.js';
14
+ import { dotnetFiles, extractDotnet } from './dotnet.js';
15
+ import { extractMobile, mobileFacts } from './mobile.js';
16
+ import { cargoConfig, extractRust, rustFiles } from './rust.js';
17
+ import { authHelperCandidates } from './candidates.js';
18
+ import { extractPython } from './py.js';
19
+ import { extractDjango } from './py-django.js';
20
+ import { langOf, Repo } from './repo.js';
21
+ // Web frameworks a built extractor reads, and ones it does not, by language group (stack.json framework ids).
22
+ const READ_BY = { 'node-ts': ['nextjs', 'express', 'fastify', 'koa', 'hono', 'nestjs', 'trpc', 'react', 'expo'], python: ['flask', 'fastapi', 'django', 'quart', 'sanic', 'starlette'], go: ['chi', 'echo', 'fiber', 'gin', 'gorilla-mux'], ruby: ['rails'], php: ['laravel'], java: ['spring', 'spring-boot'], csharp: ['aspnet-core'] };
23
+ const UNREAD = {
24
+ 'node-ts': ['adonisjs', 'apollo-server', 'hapi', 'loopback', 'remix', 'sveltekit', 'nuxt', 'astro', 'socket.io', 'graphql'],
25
+ python: ['aiohttp', 'bottle', 'falcon', 'litestar', 'pyramid', 'tornado', 'gradio', 'streamlit'],
26
+ go: [], ruby: ['sinatra', 'grape', 'hanami', 'roda'],
27
+ php: ['symfony', 'slim', 'codeigniter', 'cakephp', 'yii', 'drupal', 'wordpress'],
28
+ java: ['quarkus', 'micronaut', 'javalin'], kotlin: ['ktor', 'spring', 'spring-boot'],
29
+ csharp: [], rust: ['actix-web', 'axum', 'rocket', 'warp'], elixir: ['phoenix', 'ecto'],
30
+ dart: ['dart-frog', 'shelf'], swift: ['vapor'],
31
+ };
32
+ const SERVER_LANGS = new Set(['java', 'kotlin', 'csharp', 'rust', 'scala', 'elixir', 'swift', 'dart', 'perl', 'c', 'cpp', 'lua', 'haskell', 'clojure', 'groovy', 'erlang', 'fsharp', 'ocaml', 'julia', 'r', 'visual-basic', 'zig', 'nim']);
33
+ const inPrefix = (path, prefix) => prefix === '*' || prefix === '/' || path === prefix || path.startsWith(prefix.replace(/\/$/, '') + '/') || path.startsWith(prefix.replace(/\/$/, '') + '*');
34
+ const ROUTE_FW = new Set(['express', 'flask', 'fastapi', 'quart', 'sanic', 'flask-restful']);
35
+ const WEB_KINDS = new Set(['http_route', 'webhook', 'page']);
36
+ const confOf = (c, cap) => (c === 'low' || cap === 'low' ? 'low' : c === 'medium' || cap === 'medium' ? 'medium' : 'high');
37
+ function addProtections(ep, ps) {
38
+ const have = new Set(ep.protections.map((p) => `${p.kind}|${p.scope}|${p.file}|${p.line}|${p.via}`));
39
+ for (const p of ps) {
40
+ const k = `${p.kind}|${p.scope}|${p.file}|${p.line}|${p.via}`;
41
+ if (!have.has(k)) {
42
+ have.add(k);
43
+ ep.protections.push(p);
44
+ }
45
+ }
46
+ ep.protections.sort((a, b) => cmp(a.file, b.file) || a.line - b.line || cmp(a.kind, b.kind) || cmp(a.via, b.via) || cmp(a.scope, b.scope));
47
+ }
48
+ function prefixChain(mounts) {
49
+ const byTarget = new Map();
50
+ for (const m of mounts)
51
+ byTarget.set(m.target, [...(byTarget.get(m.target) ?? []), m]);
52
+ const resolve = (file, depth) => {
53
+ const ms = (byTarget.get(file) ?? []).slice().sort((a, b) => cmp(a.from, b.from) || a.line - b.line);
54
+ if (!ms.length || depth > 5)
55
+ return null;
56
+ const m = ms[0];
57
+ const up = resolve(m.from, depth + 1);
58
+ return { prefix: joinPath(up?.prefix ?? '', m.prefix), protections: [...(up?.protections ?? []), ...m.protections] };
59
+ };
60
+ const out = new Map();
61
+ for (const t of byTarget.keys()) {
62
+ const r = resolve(t, 0);
63
+ if (r)
64
+ out.set(t, r);
65
+ }
66
+ return out;
67
+ }
68
+ export function buildCodeMap(ctx) {
69
+ const repo = new Repo(ctx);
70
+ const all = { entries: [], gates: [], mounts: [] };
71
+ const merge = (x) => { all.entries.push(...x.entries); all.gates.push(...x.gates); all.mounts.push(...x.mounts); };
72
+ // A file the extractor throws on is skipped and counted, so the coverage row can say so.
73
+ const failed = [];
74
+ const guard = (file, f) => { try {
75
+ merge(f());
76
+ }
77
+ catch (e) {
78
+ failed.push({ file, message: e.message });
79
+ } };
80
+ for (const file of ctx.files) {
81
+ const lang = langOf(file);
82
+ if (!lang)
83
+ continue;
84
+ const v = repo.view(file);
85
+ if (!v)
86
+ continue;
87
+ if (lang === 'js') {
88
+ if (repo.isClient(v)) {
89
+ if (/(?:^|\/)page\.[cm]?[jt]sx?$/.test(file))
90
+ guard(file, () => extractNext(repo, v));
91
+ continue;
92
+ }
93
+ guard(file, () => extractNext(repo, v));
94
+ guard(file, () => extractExpress(repo, v));
95
+ guard(file, () => extractNest(repo, v));
96
+ guard(file, () => extractJobs(repo, v));
97
+ guard(file, () => extractAiTools(repo, v));
98
+ }
99
+ else if (lang === 'py')
100
+ guard(file, () => extractPython(repo, v));
101
+ }
102
+ guard('(django url configuration)', () => extractDjango(repo));
103
+ if (goFiles(repo).length)
104
+ guard('(go routes)', () => extractGoAll(repo, new FnIndex(repo, 'go'), repo.local));
105
+ if (railsFiles(repo).routes.length)
106
+ guard('(rails routes)', () => extractRailsAll(repo, new FnIndex(repo, 'rb'), repo.local));
107
+ if (laravelFiles(repo).length)
108
+ guard('(laravel routes)', () => extractLaravelAll(repo, new FnIndex(repo, 'php'), repo.local));
109
+ if (springFiles(repo).length)
110
+ guard('(spring controllers)', () => extractSpring(repo, new FnIndex(repo, 'java'), repo.local));
111
+ if (dotnetFiles(repo).length)
112
+ guard('(aspnet routes)', () => extractDotnet(repo, new FnIndex(repo, 'cs'), repo.local));
113
+ guard('(android and ios configuration)', () => extractMobile(repo));
114
+ if (rustFiles(repo).length)
115
+ guard('(rust routes)', () => extractRust(repo));
116
+ guard('(expo router)', () => extractExpo(repo));
117
+ // Router mounts: routes registered in a mounted file live under the mount prefix and inherit its protections.
118
+ const chains = prefixChain(all.mounts);
119
+ for (const ep of all.entries) {
120
+ const c = chains.get(ep.file);
121
+ if (!c || !ROUTE_FW.has(ep.framework) || ep.path === null)
122
+ continue;
123
+ ep.path = joinPath(c.prefix, ep.path);
124
+ if (ep.confidence === 'high')
125
+ ep.confidence = 'medium';
126
+ addProtections(ep, c.protections.map((p) => ({ ...p, scope: 'router' })));
127
+ }
128
+ // Gates: middleware and guards that cover an entry point by path, file order or framework.
129
+ const gates = all.gates.slice().sort((a, b) => cmp(a.file, b.file) || a.line - b.line || cmp(a.id, b.id));
130
+ for (const ep of all.entries) {
131
+ if (!WEB_KINDS.has(ep.kind) && ep.kind !== 'server_action' && ep.kind !== 'graphql_resolver' && ep.kind !== 'rpc')
132
+ continue;
133
+ for (const g of gates) {
134
+ if (g.framework === 'spring' || g.framework === 'aspnet')
135
+ continue; // the extractor applied these rules itself, in order, first match wins
136
+ if (g.framework === 'express' && (g.file !== ep.file || g.line > ep.line))
137
+ continue;
138
+ if (g.framework === 'next-app' && !(ep.framework === 'next-app' || ep.framework === 'next-pages') || (g.framework === 'next-app' && ep.kind === 'server_action'))
139
+ continue;
140
+ if (g.framework === 'flask' && ep.framework !== 'flask' && ep.framework !== 'quart')
141
+ continue;
142
+ if (g.framework === 'nest' && ep.framework !== 'nest')
143
+ continue;
144
+ if (g.framework === 'django' && ep.framework !== 'django')
145
+ continue;
146
+ if (g.methods.length && ep.method && ep.method !== 'ANY' && !g.methods.includes(ep.method))
147
+ continue;
148
+ if (ep.path === null && !g.paths.includes('*'))
149
+ continue;
150
+ if (!g.paths.some((p) => ep.path === null ? p === '*' : inPrefix(ep.path, p)))
151
+ continue;
152
+ const cap = g.paths.includes('*') || g.kind === 'guard' || g.kind === 'global_pipe' ? g.confidence : 'medium';
153
+ addProtections(ep, g.protections.map((p) => ({ ...p, scope: g.kind === 'guard' || g.kind === 'global_pipe' ? 'global' : p.scope === 'router' ? 'router' : 'middleware', confidence: confOf(p.confidence, cap) })));
154
+ }
155
+ }
156
+ // Vercel cron paths: an entry point called by the platform scheduler.
157
+ try {
158
+ const vj = JSON.parse(ctx.text('vercel.json') || '{}');
159
+ for (const c of vj.crons ?? [])
160
+ for (const ep of all.entries)
161
+ if (c.path && ep.path && ep.path === joinPath('', c.path.split('?')[0]))
162
+ ep.tags = [...new Set([...ep.tags, 'cron'])].sort();
163
+ }
164
+ catch { /* a vercel.json that does not parse adds no cron tags */ }
165
+ const seen = new Map();
166
+ for (const ep of all.entries)
167
+ if (!seen.has(ep.id))
168
+ seen.set(ep.id, ep);
169
+ const entries = [...seen.values()].sort((a, b) => cmp(a.file, b.file) || a.line - b.line || cmp(a.method ?? '', b.method ?? '') || cmp(a.path ?? '', b.path ?? '') || cmp(a.name ?? '', b.name ?? '') || cmp(a.kind, b.kind));
170
+ const by = (key) => { const o = {}; for (const e of entries)
171
+ o[key(e)] = (o[key(e)] ?? 0) + 1; return Object.fromEntries(Object.entries(o).sort(([a], [b]) => cmp(a, b))); };
172
+ const facts = mobileFacts(repo);
173
+ return {
174
+ schema_version: '1.0.0', engine_version: ENGINE_VERSION, commit: ctx.stack.commit,
175
+ coverage: coverage(ctx, entries, failed, facts, repo), entry_points: entries,
176
+ gates: gates.map((g) => ({ ...g, protections: g.protections.slice().sort((a, b) => a.line - b.line || cmp(a.kind, b.kind) || cmp(a.via, b.via)) })),
177
+ config: { supabase: supabaseMap(repo), firebase: firebaseMap(repo), android: facts.android, ios: facts.ios, cargo: cargoConfig(repo), secrets: facts.secrets, local_vocab: { auth_helpers: repo.local.auth_helpers, role_helpers: repo.local.role_helpers }, auth_helper_candidates: authHelperCandidates(repo, entries) },
178
+ counts: { entry_points: entries.length, by_kind: by((e) => e.kind), by_framework: by((e) => e.framework) },
179
+ };
180
+ }
181
+ function coverage(ctx, entries, failed, facts, repo) {
182
+ const frameworks = new Set([...ctx.stack.frameworks.map((f) => f.id), ...(ctx.stack.mobile ?? []).map((f) => f.id)]);
183
+ const rows = [];
184
+ const nodeTotal = entries.filter((e) => /\.[cm]?[jt]sx?$/.test(e.file)).length;
185
+ const count = (re) => entries.filter((e) => re.test(e.file)).length;
186
+ for (const l of ctx.stack.languages) {
187
+ const files = l.file_count ?? 0;
188
+ if (l.id === 'javascript' || l.id === 'typescript') {
189
+ const n = count(l.id === 'typescript' ? /\.[cm]?tsx?$/ : /\.[cm]?jsx?$/);
190
+ const unread = UNREAD['node-ts'].filter((f) => frameworks.has(f));
191
+ rows.push({ language: l.id, files, extractor: 'node-ts', status: unread.length ? 'partial' : 'mapped', entry_points: n, note: unread.length ? `Frameworks seen that this extractor does not read: ${unread.join(', ')}. Entry points they register may be missing.` : n === 0 && nodeTotal === 0 && READ_BY['node-ts'].some((f) => frameworks.has(f) && f !== 'react') ? 'A web framework was detected and no entry point was found. The routes may be registered in a way the extractor does not read.' : frameworks.has('expo') ? 'Next.js, Express family, NestJS, tRPC, Expo Router screens, API routes and deep links, jobs and AI tools are read.' : 'Next.js, Express family, NestJS, tRPC, jobs and AI tools are read.' });
192
+ }
193
+ else if (l.id === 'python') {
194
+ const n = count(/\.py$/);
195
+ const unread = UNREAD.python.filter((f) => frameworks.has(f));
196
+ rows.push({ language: l.id, files, extractor: 'python', status: unread.length ? 'partial' : 'mapped', entry_points: n, note: unread.length ? `Frameworks seen that this extractor does not read: ${unread.join(', ')}. Entry points they register may be missing.` : n === 0 && READ_BY.python.some((f) => frameworks.has(f)) ? 'A web framework was detected and no entry point was found. The routes may be registered in a way the extractor does not read.' : 'Flask, FastAPI, Django and REST framework, Celery and scheduler jobs, and AI tools are read.' });
197
+ }
198
+ else if (l.id === 'go' || l.id === 'ruby' || l.id === 'php') {
199
+ const ext = l.id === 'go' ? /\.go$/ : l.id === 'ruby' ? /\.rb$/ : /\.php$/;
200
+ const n = count(ext);
201
+ const unread = UNREAD[l.id].filter((f) => frameworks.has(f));
202
+ const readSeen = READ_BY[l.id].filter((f) => frameworks.has(f));
203
+ const name = l.id === 'go' ? 'go' : l.id === 'ruby' ? 'rails' : 'laravel';
204
+ const what = l.id === 'go' ? 'net/http, chi, gin, echo, fiber and gorilla/mux routes' : l.id === 'ruby' ? 'Rails routes (config/routes.rb) and controller filters' : 'Laravel routes (routes/*.php) and controller middleware';
205
+ let status = unread.length ? 'partial' : 'mapped';
206
+ let note = unread.length ? `Frameworks seen that this extractor does not read: ${unread.join(', ')}. Entry points they register may be missing.` : `${what[0].toUpperCase()}${what.slice(1)} are read.`;
207
+ if (l.id !== 'go' && !readSeen.length && n === 0 && !unread.length) {
208
+ status = 'partial';
209
+ note = `No ${l.id === 'ruby' ? 'Rails' : 'Laravel'} project was found, so no routes were read. Web code in ${l.id} files is not in this map.`;
210
+ }
211
+ else if (readSeen.length && n === 0)
212
+ note = 'A web framework was detected and no entry point was found. The routes may be registered in a way the extractor does not read.';
213
+ rows.push({ language: l.id, files, extractor: name, status, entry_points: n, note });
214
+ }
215
+ else if (l.id === 'java') {
216
+ const n = count(/\.java$/);
217
+ const unread = UNREAD.java.filter((f) => frameworks.has(f));
218
+ const springSeen = READ_BY.java.some((f) => frameworks.has(f));
219
+ rows.push({ language: 'java', files, extractor: 'spring', status: unread.length || !springSeen ? 'partial' : 'mapped', entry_points: n,
220
+ note: unread.length ? `Frameworks seen that this extractor does not read: ${unread.join(', ')}. Entry points they register may be missing.` : !springSeen ? 'No Spring project was found, so no routes were read. Web code in Java files is not in this map.' : n === 0 ? 'A web framework was detected and no entry point was found. The routes may be registered in a way the extractor does not read.' : 'Spring MVC controllers (@RestController, @Controller and the mapping annotations) are read, with method security annotations and the Spring Security filter chain rules.' });
221
+ }
222
+ else if (l.id === 'csharp') {
223
+ const n = count(/\.cs$/);
224
+ const aspSeen = READ_BY.csharp.some((f) => frameworks.has(f));
225
+ rows.push({ language: 'csharp', files, extractor: 'aspnet', status: aspSeen ? 'mapped' : 'partial', entry_points: n,
226
+ note: !aspSeen ? 'No ASP.NET Core project was found, so no routes were read. Web code in C# files is not in this map.' : n === 0 ? 'A web framework was detected and no entry point was found. The routes may be registered in a way the extractor does not read.' : 'ASP.NET Core controllers ([Route] and the [HttpGet] family) and minimal APIs (MapGet and the others, with MapGroup) are read, with [Authorize], [AllowAnonymous], RequireAuthorization and a fallback policy.' });
227
+ }
228
+ else if (l.id === 'kotlin' || l.id === 'swift' || l.id === 'rust') {
229
+ const fw = l.id === 'kotlin' ? 'android' : l.id === 'swift' ? 'ios' : 'axum|actix-web';
230
+ const n = entries.filter((e) => new RegExp(`^(?:${fw})$`).test(e.framework)).length;
231
+ const what = l.id === 'kotlin' ? 'AndroidManifest.xml (exported components and deep links) and the network security configuration' : l.id === 'swift' ? 'Info.plist (URL schemes, App Transport Security) and the entitlements (associated domains)' : 'Cargo.toml, and axum and actix web routes';
232
+ const read = l.id === 'rust' ? rustFiles(repo).length > 0 || (cargoConfig(repo)?.manifests.length ?? 0) > 0 : l.id === 'kotlin' ? (facts.android?.manifests.length ?? 0) > 0 : (facts.ios?.plists.length ?? 0) > 0;
233
+ rows.push(read
234
+ ? { language: l.id, files, extractor: l.id === 'kotlin' ? 'android' : l.id === 'swift' ? 'ios' : 'rust', status: 'partial', entry_points: n, note: `${what[0].toUpperCase()}${what.slice(1)} are read. ${l.id === 'rust' ? 'Handler bodies are not read.' : 'The source files are not read, so what a handler does with an incoming intent or URL is not in this map.'}` }
235
+ : { language: l.id, files, extractor: null, status: 'not_mapped', entry_points: 0, note: `No extractor input found for ${l.id}${(UNREAD[l.id] ?? []).filter((f) => frameworks.has(f)).length ? ` (frameworks seen: ${(UNREAD[l.id] ?? []).filter((f) => frameworks.has(f)).join(', ')})` : ''}. Entry points in ${l.id} files are not in this map.` });
236
+ }
237
+ else if (SERVER_LANGS.has(l.id)) {
238
+ const seen = (UNREAD[l.id] ?? []).filter((f) => frameworks.has(f));
239
+ rows.push({ language: l.id, files, extractor: null, status: 'not_mapped', entry_points: 0, note: `No extractor for ${l.id} yet${seen.length ? ` (frameworks seen: ${seen.join(', ')})` : ''}. Entry points in ${l.id} files are not in this map.` });
240
+ }
241
+ }
242
+ // Files an extractor threw on: the language they belong to is only partly mapped.
243
+ const langOfFailed = (file) => (/\.py$/.test(file) || /django/.test(file) ? 'python' : /\.go$|\(go /.test(file) ? 'go' : /\.rb$|\(rails /.test(file) ? 'ruby' : /\.php$|\(laravel /.test(file) ? 'php' : /\.tsx?$/.test(file) ? 'typescript' : 'javascript');
244
+ for (const f of failed) {
245
+ const lang = langOfFailed(f.file);
246
+ const row = rows.find((r) => r.language === lang) ?? rows[0];
247
+ if (row && !row.note.includes('could not be read')) {
248
+ row.status = 'partial';
249
+ row.note += ` ${failed.filter((x) => langOfFailed(x.file) === lang).length} file(s) could not be read by the extractor, first: ${f.file}.`;
250
+ }
251
+ }
252
+ return rows.sort((a, b) => cmp(a.language, b.language));
253
+ }
@@ -0,0 +1,49 @@
1
+ const AUTHISH = /auth|login|logged|session|token|jwt|bearer|guard|protect|permission|member|tenant|signed|credential|principal|identity|viewer|whoami|current|require|ensure|verify|owner|account|user/i;
2
+ const SKIP = /^(?:console|log|logger|status|json|send|redirect|render|parse|stringify|then|catch|require|import|function|return|if|for|while|switch|await|async|new|super|print|puts|len|str|int|dict|list|set|get|post|put|patch|delete|use|map|filter|reduce|find|from|select|where|insert|update|create|emit|on|once|next|type|isinstance|self|this|params|param|query|body|request|response|req|res|ctx|c|r|w|id|name|email|password|user|users|account|accounts|session|token|userId|user_id|accountId|find_by|find_by_id|findOne|findById|first|last|all|count|exists|save|destroy|build|new_record|dispatch|useState|useEffect|useMemo|useCallback|useRef|useRouter|useParams|useSearchParams|useLocalSearchParams|useNavigation|useRoute|useContext|useQuery|useMutation|useTranslation|useForm|useWatch|useTheme|useColorScheme|useWindowDimensions)$/;
3
+ const MAX = 25;
4
+ export function authHelperCandidates(repo, entries) {
5
+ const seen = new Map();
6
+ const judged = entries.filter((e) => !e.test_code && ['http_route', 'server_action', 'graphql_resolver', 'rpc'].includes(e.kind) && e.kind !== 'webhook' && (e.tags.includes('state_changing') || e.data.length > 0)
7
+ && !e.tags.includes('auth_endpoint') && !e.tags.includes('public') && !e.protections.some((p) => p.kind === 'session_read' && p.confidence !== 'low' && p.enforced !== false));
8
+ for (const e of judged) {
9
+ const file = e.handler?.file ?? e.file;
10
+ const first = e.handler?.line ?? e.line;
11
+ const lines = repo.ctx.text(file).split('\n').slice(Math.max(0, first - 4), Math.max(first, e.end_line));
12
+ const names = new Set();
13
+ const ruby = /\.rb$/.test(file);
14
+ if (ruby) {
15
+ // Class level filters of the controller that run before this action.
16
+ const action = e.name?.split('#')[1];
17
+ for (const fl of repo.ctx.text(file).matchAll(/^[ \t]*(?:prepend_)?before_action\b([^\n]*)$/gm)) {
18
+ const only = /\bonly:\s*(%i\[([^\]]*)\]|\[([^\]]*)\]|:(\w+))/.exec(fl[1]);
19
+ const except = /\bexcept:\s*(%i\[([^\]]*)\]|\[([^\]]*)\]|:(\w+))/.exec(fl[1]);
20
+ const list = (m) => (m ? (m[2] ?? m[3] ?? m[4] ?? '').split(/[\s,]+/).map((x) => x.replace(/^:/, '')).filter(Boolean) : []);
21
+ if (action && ((only && !list(only).includes(action)) || (except && list(except).includes(action))))
22
+ continue;
23
+ for (const n of fl[1].replace(/\b(?:only|except|if|unless)\s*:.*$/, '').matchAll(/:(\w+[?!]?)/g))
24
+ names.add(n[1]);
25
+ }
26
+ }
27
+ for (const l of lines) {
28
+ if (/^\s*(?:func|def|function|public|private|protected|class|module|before_action|prepend_before_action|skip_before_action|skip_before_filter|allow_unauthenticated_access)\b/.test(l))
29
+ continue;
30
+ const code = l.replace(/\/\/.*$|#(?!\[).*$/, '');
31
+ for (const m of code.matchAll(ruby ? /(?:@|->|::|\.|\b)([A-Za-z_$][\w$]*[!?]?)\s*(?:\(|\b(?=\s*$)|(?=\s*,|\s*\)))/g : /(?:@|->|::|\.|\b)([A-Za-z_$][\w$]*[!?]?)\s*(?:\(|(?=\s*,|\s*\)))/g))
32
+ names.add(m[1]);
33
+ for (const m of code.matchAll(/:(\w+[!?]?)/g))
34
+ if (/^\s*(?:before_action|skip_before_action)/.test(code))
35
+ names.add(m[1]);
36
+ }
37
+ for (const n of names) {
38
+ if (n.length < 4 || SKIP.test(n) || !AUTHISH.test(n))
39
+ continue;
40
+ const cur = seen.get(n);
41
+ if (cur)
42
+ cur.n++;
43
+ else
44
+ seen.set(n, { n: 1, file: e.file, line: e.line });
45
+ }
46
+ }
47
+ return [...seen.entries()].map(([name, v]) => ({ name, entry_points: v.n, file: v.file, line: v.line }))
48
+ .sort((a, b) => b.entry_points - a.entry_points || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)).slice(0, MAX);
49
+ }
@@ -0,0 +1,77 @@
1
+ // Shared reading helpers for the annotation based languages (Java and Kotlin in Spring, C# attributes): split a class body into
2
+ // declaration blocks, each with the annotations in front of it, and read string literals out of annotation arguments. Works on masked
3
+ // text for structure and on the original text for the string values, which sit at the same positions.
4
+ import { closeOf } from './lex.js';
5
+ // Annotations of the form @Name or @Name(...) at brace depth 0 of the range [from, to), grouped into blocks of consecutive annotations.
6
+ // `declStart` is the first character after the last annotation of the block (the declaration the annotations belong to).
7
+ export function annotationBlocks(masked, from, to) {
8
+ const out = [];
9
+ let depth = 0;
10
+ let cur = null;
11
+ for (let i = from; i < to; i++) {
12
+ const c = masked[i];
13
+ if (c === '{' || c === '(' || c === '[') {
14
+ depth++;
15
+ continue;
16
+ }
17
+ if (c === '}' || c === ')' || c === ']') {
18
+ depth--;
19
+ continue;
20
+ }
21
+ if (depth !== 0 || c !== '@')
22
+ continue;
23
+ const m = /^@([A-Za-z_][\w.]*)/.exec(masked.slice(i, i + 80));
24
+ if (!m)
25
+ continue;
26
+ let end = i + m[0].length;
27
+ let args = null;
28
+ let k = end;
29
+ while (/[ \t]/.test(masked[k] ?? ''))
30
+ k++;
31
+ if (masked[k] === '(') {
32
+ const close = closeOf(masked, k);
33
+ if (close > 0) {
34
+ args = { start: k + 1, end: close - 1 };
35
+ end = close;
36
+ }
37
+ }
38
+ const gap = cur ? masked.slice(cur.annos[cur.annos.length - 1].end, i) : '';
39
+ if (cur && /^\s*$/.test(gap))
40
+ cur.annos.push({ name: m[1], start: i, end, args });
41
+ else {
42
+ cur = { annos: [{ name: m[1], start: i, end, args }], start: i, end: 0, declStart: 0 };
43
+ out.push(cur);
44
+ }
45
+ i = end - 1;
46
+ }
47
+ for (const b of out) {
48
+ b.end = b.annos[b.annos.length - 1].end;
49
+ let d = b.end;
50
+ while (/\s/.test(masked[d] ?? ''))
51
+ d++;
52
+ b.declStart = d;
53
+ }
54
+ return out;
55
+ }
56
+ // All "string" literals in the text, with the escapes of a quoted string left as written.
57
+ export const strings = (text) => [...text.matchAll(/"((?:[^"\\\n]|\\.)*)"/g)].map((m) => m[1]);
58
+ // Top level comma split of a parameter or argument list (commas inside (), [], {} and <> do not split).
59
+ export function splitTop(text) {
60
+ const out = [];
61
+ let depth = 0;
62
+ let from = 0;
63
+ for (let i = 0; i < text.length; i++) {
64
+ const c = text[i];
65
+ if (c === '(' || c === '[' || c === '{' || c === '<')
66
+ depth++;
67
+ else if (c === ')' || c === ']' || c === '}' || (c === '>' && text[i - 1] !== '=' && text[i - 1] !== '-'))
68
+ depth = Math.max(0, depth - 1);
69
+ else if (c === ',' && depth === 0) {
70
+ out.push(text.slice(from, i));
71
+ from = i + 1;
72
+ }
73
+ }
74
+ if (text.slice(from).trim())
75
+ out.push(text.slice(from));
76
+ return out.map((x) => x.trim()).filter(Boolean);
77
+ }