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,282 @@
1
+ // Fact extractors: declared dependencies from manifests, and imports from source files. Small built in readers only,
2
+ // line based where the format allows, so every fact carries a file and a line (or a JSON key).
3
+ const base = (f) => f.slice(f.lastIndexOf('/') + 1);
4
+ const lines = (t) => t.split(/\r?\n/);
5
+ const pyName = (n) => n.toLowerCase().replace(/_/g, '-');
6
+ function jsonDeps(eco, file, text, sections) {
7
+ let j;
8
+ try {
9
+ j = JSON.parse(text);
10
+ }
11
+ catch {
12
+ return [];
13
+ }
14
+ const out = [];
15
+ for (const s of sections) {
16
+ const deps = j?.[s];
17
+ if (!deps || typeof deps !== 'object')
18
+ continue;
19
+ for (const [name, v] of Object.entries(deps)) {
20
+ out.push({ eco, name, version: typeof v === 'string' ? v : null, file, key: `${s}.${name}` });
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+ function eachLine(text, fn) {
26
+ lines(text).forEach((l, i) => fn(l, i + 1));
27
+ }
28
+ function requirementString(s) {
29
+ const m = /^\s*([A-Za-z0-9][A-Za-z0-9_.-]*)(?:\[[^\]]*\])?\s*((?:===|[<>=!~]=?)[^;#,\s]*(?:\s*,\s*[<>=!~]=?[^;#,\s]*)*)?\s*(?:;.*)?$/.exec(s);
30
+ return m ? { name: pyName(m[1]), version: m[2] ?? null } : null;
31
+ }
32
+ function requirementsTxt(file, text) {
33
+ const out = [];
34
+ eachLine(text, (l, n) => {
35
+ const t = l.replace(/\s+#.*$/, '').trim();
36
+ if (!t || t.startsWith('#') || t.startsWith('-') || t.includes('://'))
37
+ return;
38
+ const r = requirementString(t);
39
+ if (r)
40
+ out.push({ eco: 'pypi', ...r, file, line: n });
41
+ });
42
+ return out;
43
+ }
44
+ function pyproject(file, text) {
45
+ const out = [];
46
+ let section = '';
47
+ let arrayKey = null;
48
+ eachLine(text, (l, n) => {
49
+ const sec = /^\s*\[{1,2}([^\]]+)\]{1,2}\s*$/.exec(l);
50
+ if (sec) {
51
+ section = sec[1].trim();
52
+ arrayKey = null;
53
+ return;
54
+ }
55
+ let rest = l;
56
+ if (arrayKey === null) {
57
+ const a = /^\s*"?([A-Za-z0-9_.-]+)"?\s*=\s*\[(.*)$/.exec(l);
58
+ if (a) {
59
+ arrayKey = a[1];
60
+ rest = a[2];
61
+ }
62
+ else if (/(^|\.)dependencies$/.test(section) && section !== 'project') {
63
+ const p = /^\s*([A-Za-z0-9_.-]+)\s*=\s*(.*)$/.exec(l); // poetry style: name = "^1.0" or { version = "1.0" }
64
+ if (p && p[1].toLowerCase() !== 'python') {
65
+ const v = /^"([^"]*)"/.exec(p[2])?.[1] ?? /version\s*=\s*"([^"]*)"/.exec(p[2])?.[1] ?? null;
66
+ out.push({ eco: 'pypi', name: pyName(p[1]), version: v, file, line: n });
67
+ }
68
+ return;
69
+ }
70
+ else
71
+ return;
72
+ }
73
+ const depArray = (section === 'project' && arrayKey === 'dependencies') || section === 'project.optional-dependencies'
74
+ || section === 'dependency-groups' || (section === 'build-system' && arrayKey === 'requires');
75
+ if (depArray) {
76
+ for (const m of rest.matchAll(/"([^"]+)"|'([^']+)'/g)) {
77
+ const r = requirementString(m[1] ?? m[2]);
78
+ if (r)
79
+ out.push({ eco: 'pypi', ...r, file, line: n });
80
+ }
81
+ }
82
+ if (rest.includes(']'))
83
+ arrayKey = null;
84
+ });
85
+ return out;
86
+ }
87
+ function sectionKeyValues(eco, file, text, isDepSection, norm) {
88
+ const out = [];
89
+ let section = '';
90
+ eachLine(text, (l, n) => {
91
+ const sec = /^\s*\[([^\]]+)\]\s*$/.exec(l);
92
+ if (sec) {
93
+ section = sec[1].trim();
94
+ return;
95
+ }
96
+ if (!isDepSection(section))
97
+ return;
98
+ const p = /^\s*"?([A-Za-z0-9_.-]+)"?\s*=\s*(.*)$/.exec(l);
99
+ if (!p)
100
+ return;
101
+ const v = /^"([^"]*)"/.exec(p[2])?.[1] ?? /version\s*=\s*"([^"]*)"/.exec(p[2])?.[1] ?? null;
102
+ out.push({ eco, name: norm(p[1]), version: v, file, line: n });
103
+ });
104
+ return out;
105
+ }
106
+ function regexLines(eco, file, text, re, nameOf, verOf) {
107
+ const out = [];
108
+ eachLine(text, (l, n) => {
109
+ const m = re.exec(l);
110
+ if (m)
111
+ out.push({ eco, name: nameOf(m), version: verOf(m), file, line: n });
112
+ });
113
+ return out;
114
+ }
115
+ function goMod(file, text) {
116
+ const out = [];
117
+ let inBlock = false;
118
+ eachLine(text, (l, n) => {
119
+ if (/^\s*require\s*\(\s*$/.test(l)) {
120
+ inBlock = true;
121
+ return;
122
+ }
123
+ if (inBlock && /^\s*\)/.test(l)) {
124
+ inBlock = false;
125
+ return;
126
+ }
127
+ const m = inBlock ? /^\s*(\S+)\s+(v\S+)/.exec(l) : /^\s*require\s+(\S+)\s+(v\S+)/.exec(l);
128
+ if (m)
129
+ out.push({ eco: 'go', name: m[1], version: m[2], file, line: n });
130
+ });
131
+ return out;
132
+ }
133
+ function pomXml(file, text) {
134
+ const out = [];
135
+ for (const m of text.matchAll(/<(dependency|parent|plugin)>([\s\S]*?)<\/\1>/g)) {
136
+ const body = m[2];
137
+ const g = /<groupId>\s*([^<\s]+)\s*<\/groupId>/.exec(body)?.[1] ?? '';
138
+ const a = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(body);
139
+ if (!a)
140
+ continue;
141
+ const v = /<version>\s*([^<\s]+)\s*<\/version>/.exec(body)?.[1] ?? null;
142
+ const at = (m.index ?? 0) + m[0].indexOf(a[0]);
143
+ out.push({ eco: 'maven', name: `${g}:${a[1]}`, version: v, file, line: text.slice(0, at).split('\n').length });
144
+ }
145
+ return out;
146
+ }
147
+ function pubspec(file, text) {
148
+ const out = [];
149
+ let inDeps = false;
150
+ eachLine(text, (l, n) => {
151
+ if (/^\S/.test(l)) {
152
+ inDeps = /^(dependencies|dev_dependencies|dependency_overrides):\s*$/.test(l);
153
+ return;
154
+ }
155
+ if (!inDeps)
156
+ return;
157
+ const m = /^ {2}([A-Za-z0-9_]+):\s*(.*)$/.exec(l);
158
+ if (m)
159
+ out.push({ eco: 'pub', name: m[1], version: m[2].trim() || null, file, line: n });
160
+ });
161
+ return out;
162
+ }
163
+ function terraform(file, text) {
164
+ const out = [];
165
+ eachLine(text, (l, n) => {
166
+ // Provider blocks, and the provider prefix of resource and data types (aws_s3_bucket is provider aws).
167
+ const p = /^\s*provider\s+"([a-z0-9_-]+)"/.exec(l) ?? /^\s*(?:resource|data)\s+"([a-z0-9]+)_/.exec(l);
168
+ if (p)
169
+ out.push({ eco: 'terraform', name: p[1], version: null, file, line: n });
170
+ });
171
+ return out;
172
+ }
173
+ // Every manifest format the engine reads, by file name.
174
+ export function extractDeps(file, text) {
175
+ const b = base(file);
176
+ if (b === 'package.json')
177
+ return jsonDeps('npm', file, text, ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']);
178
+ if (b === 'composer.json')
179
+ return jsonDeps('composer', file, text, ['require', 'require-dev']);
180
+ if (/^requirements.*\.(txt|in)$/.test(b) || /(^|\/)requirements\/[^/]+\.(txt|in)$/.test(file))
181
+ return requirementsTxt(file, text);
182
+ if (b === 'pyproject.toml')
183
+ return pyproject(file, text);
184
+ if (b === 'Pipfile')
185
+ return sectionKeyValues('pypi', file, text, (s) => s === 'packages' || s === 'dev-packages', pyName);
186
+ if (b === 'setup.py')
187
+ return regexLines('pypi', file, text, /^\s*['"]([A-Za-z][A-Za-z0-9_.-]*)(?:\[[^\]]*\])?\s*((?:[<>=!~]=?)[^'"]*)?['"],?\s*$/, (m) => pyName(m[1]), (m) => m[2] ?? null);
188
+ if (b === 'go.mod')
189
+ return goMod(file, text);
190
+ if (b === 'Cargo.toml')
191
+ return sectionKeyValues('cargo', file, text, (s) => /(^|\.)(dev-|build-)?dependencies$/.test(s), (s) => s);
192
+ if (b === 'Gemfile')
193
+ return regexLines('rubygems', file, text, /^\s*gem\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/, (m) => m[1], (m) => m[2] ?? null);
194
+ if (b === 'pom.xml')
195
+ return pomXml(file, text);
196
+ if (b === 'build.gradle' || b === 'build.gradle.kts') {
197
+ return regexLines('maven', file, text, /\b(?:implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|androidTestImplementation|kapt|ksp|annotationProcessor|classpath|compile)\s*\(?\s*(?:platform\s*\(\s*)?['"]([^'":\s]+):([^'":\s]+)(?::([^'"\s]+))?['"]/, (m) => `${m[1]}:${m[2]}`, (m) => m[3] ?? null);
198
+ }
199
+ if (b === 'pubspec.yaml')
200
+ return pubspec(file, text);
201
+ if (/\.(cs|fs|vb)proj$/.test(b))
202
+ return regexLines('nuget', file, text, /<PackageReference\s+Include="([^"]+)"(?:\s+Version="([^"]+)")?/i, (m) => m[1], (m) => m[2] ?? null);
203
+ if (b === 'packages.config')
204
+ return regexLines('nuget', file, text, /<package\s+id="([^"]+)"\s+version="([^"]+)"/i, (m) => m[1], (m) => m[2]);
205
+ if (b === 'Package.swift') {
206
+ return regexLines('swiftpm', file, text, /\.package\s*\([^)]*?url:\s*"(?:https?:\/\/|git@)?([^"]+?)(?:\.git)?"(?:[^)]*?(?:from|exact):\s*"([^"]+)")?/, (m) => m[1].replace(':', '/').toLowerCase(), (m) => m[2] ?? null);
207
+ }
208
+ if (b === 'Podfile')
209
+ return regexLines('cocoapods', file, text, /^\s*pod\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/, (m) => m[1], (m) => m[2] ?? null);
210
+ if (b === 'mix.exs')
211
+ return regexLines('hex', file, text, /\{\s*:([a-z0-9_]+)\s*,\s*"([^"]+)"/, (m) => m[1], (m) => m[2]);
212
+ if (b.endsWith('.tf'))
213
+ return terraform(file, text);
214
+ return [];
215
+ }
216
+ export const MANIFEST_NAMES = /(^|\/)(package\.json|composer\.json|requirements[^/]*\.(txt|in)|requirements\/[^/]+\.(txt|in)|pyproject\.toml|Pipfile|setup\.py|go\.mod|Cargo\.toml|Gemfile|pom\.xml|build\.gradle(\.kts)?|pubspec\.yaml|[^/]+\.(cs|fs|vb)proj|packages\.config|Package\.swift|Podfile|mix\.exs|[^/]+\.tf)$/;
217
+ export const IMPORT_LANGS = {
218
+ js: { exts: ['js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'mts', 'cts', 'vue', 'svelte', 'astro'], sep: '/',
219
+ res: [/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*)['"]([^'"\s]+)['"]/g] },
220
+ python: { exts: ['py'], sep: '.', res: [/^\s*import\s+([A-Za-z_][\w.]*)/g, /^\s*from\s+([A-Za-z_][\w.]*)\s+import\b/g] },
221
+ go: { exts: ['go'], sep: '/', res: [/^\s*import\s+(?:[\w.]+\s+)?"([^"]+)"/g],
222
+ block: { start: /^\s*import\s*\(\s*$/, end: /^\s*\)/, item: /^\s*(?:[\w.]+\s+)?"([^"]+)"/ } },
223
+ ruby: { exts: ['rb'], sep: '/', res: [/^\s*require\s+['"]([^'"]+)['"]/g] },
224
+ php: { exts: ['php'], sep: '\\', res: [/^\s*use\s+\\?([A-Za-z_][\w\\]*)/g] },
225
+ jvm: { exts: ['java', 'kt', 'scala', 'groovy'], sep: '.', res: [/^\s*import\s+(?:static\s+)?([\w.]+)/g] },
226
+ swift: { exts: ['swift'], sep: '.', res: [/^\s*(?:@testable\s+)?import\s+(?:(?:class|struct|enum|protocol|func|var|typealias)\s+)?(\w+)/g] },
227
+ dart: { exts: ['dart'], sep: '/', res: [/^\s*import\s+['"](?:package:)?((?:dart:)?[^'"]+)['"]/g] },
228
+ csharp: { exts: ['cs'], sep: '.', res: [/^\s*(?:global\s+)?using\s+(?:static\s+)?([A-Z][\w.]*)\s*;/g] },
229
+ rust: { exts: ['rs'], sep: '::', res: [/^\s*(?:pub\s+)?use\s+([a-z_]\w*)::/g, /^\s*extern\s+crate\s+(\w+)/g] },
230
+ };
231
+ const LANG_BY_EXT = new Map();
232
+ for (const [k, v] of Object.entries(IMPORT_LANGS))
233
+ for (const e of v.exts)
234
+ LANG_BY_EXT.set(e, k);
235
+ export const importLangOf = (file) => {
236
+ const b = base(file);
237
+ const dot = b.lastIndexOf('.');
238
+ return dot > 0 ? LANG_BY_EXT.get(b.slice(dot + 1).toLowerCase()) ?? null : null;
239
+ };
240
+ // First line per module per file. Relative and alias imports (".", "/", "~", "@/", "#") are dropped.
241
+ export function extractImports(file, text) {
242
+ const lang = importLangOf(file);
243
+ if (!lang)
244
+ return [];
245
+ const spec = IMPORT_LANGS[lang];
246
+ const seen = new Map();
247
+ let inBlock = false;
248
+ eachLine(text, (l, n) => {
249
+ const found = [];
250
+ if (spec.block) {
251
+ if (!inBlock && spec.block.start.test(l)) {
252
+ inBlock = true;
253
+ return;
254
+ }
255
+ if (inBlock) {
256
+ if (spec.block.end.test(l)) {
257
+ inBlock = false;
258
+ return;
259
+ }
260
+ const m = spec.block.item.exec(l);
261
+ if (m)
262
+ found.push(m[1]);
263
+ }
264
+ }
265
+ for (const re of spec.res)
266
+ for (const m of l.matchAll(re))
267
+ found.push(m[1]);
268
+ for (const mod of found) {
269
+ if (lang === 'js' && /^(\.|\/|~|@\/|#)/.test(mod))
270
+ continue;
271
+ if (!seen.has(mod))
272
+ seen.set(mod, n);
273
+ }
274
+ });
275
+ return [...seen].map(([module, line]) => ({ lang, module, file, line }));
276
+ }
277
+ // Does a fact name match a rule pattern? Exact, or a prefix on the separator, or "prefix*".
278
+ export function nameMatches(name, pattern, sep) {
279
+ if (pattern.endsWith('*'))
280
+ return name.startsWith(pattern.slice(0, -1));
281
+ return name === pattern || (sep !== '' && name.startsWith(pattern + sep));
282
+ }
@@ -0,0 +1,40 @@
1
+ // Reads the HEAD commit from the .git folder without running git.
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ const SHA = /^[0-9a-f]{40}$/;
5
+ const read = (p) => { try {
6
+ return readFileSync(p, 'utf8');
7
+ }
8
+ catch {
9
+ return null;
10
+ } };
11
+ export function readHeadCommit(root) {
12
+ let gitDir = join(root, '.git');
13
+ if (!existsSync(gitDir))
14
+ return null;
15
+ if (statSync(gitDir).isFile()) {
16
+ const m = /^gitdir:\s*(.+)$/m.exec(read(gitDir) ?? '');
17
+ if (!m)
18
+ return null;
19
+ gitDir = resolve(root, m[1].trim());
20
+ }
21
+ const head = (read(join(gitDir, 'HEAD')) ?? '').trim();
22
+ if (SHA.test(head))
23
+ return head;
24
+ const ref = /^ref:\s*(\S+)$/.exec(head)?.[1];
25
+ if (!ref)
26
+ return null;
27
+ const common = read(join(gitDir, 'commondir'));
28
+ const dirs = [gitDir, ...(common ? [resolve(gitDir, common.trim())] : [])];
29
+ for (const d of dirs) {
30
+ const loose = (read(join(d, ref)) ?? '').trim();
31
+ if (SHA.test(loose))
32
+ return loose;
33
+ for (const line of (read(join(d, 'packed-refs')) ?? '').split('\n')) {
34
+ const [sha, name] = line.trim().split(' ');
35
+ if (name === ref && SHA.test(sha))
36
+ return sha;
37
+ }
38
+ }
39
+ return null;
40
+ }
@@ -0,0 +1,47 @@
1
+ // Files of the Hullproof kit that were copied into the scanned project and are still byte for byte what the kit says.
2
+ // They hold planted samples on purpose (the kit's own test files, hook self tests), so scanning them reports the kit's samples as findings in the
3
+ // user's project. The list comes from the manifest of the kit the run was given (--kit), never from a manifest inside the scanned project, because a
4
+ // project could name its own files in a manifest of its own and hide them from the scan. A file that differs by one byte is not skipped.
5
+ import { createHash } from 'node:crypto';
6
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
7
+ import { dirname, join, relative, resolve, sep } from 'node:path';
8
+ const MANIFEST_REL = 'docs/hullproof/.kit-manifest';
9
+ const LINE = /^([0-9a-f]{64}) (\S.*)$/;
10
+ export function kitOwnedFiles(repo, kitDir) {
11
+ const out = new Set();
12
+ if (!kitDir)
13
+ return out;
14
+ let manifest;
15
+ try {
16
+ manifest = readFileSync(join(kitDir, MANIFEST_REL), 'utf8');
17
+ }
18
+ catch {
19
+ return out;
20
+ }
21
+ // A kit folder inside the scanned project is the project's own content, so its manifest is not a trusted list.
22
+ try {
23
+ const rel = relative(realpathSync(resolve(repo)), realpathSync(resolve(kitDir)));
24
+ if (rel === '' || (!rel.startsWith('..') && !rel.startsWith(sep) && rel.split(sep)[0] !== '..'))
25
+ return out;
26
+ }
27
+ catch {
28
+ return out;
29
+ }
30
+ for (const line of manifest.split('\n')) {
31
+ const m = LINE.exec(line);
32
+ if (!m || m[2].includes('..') || m[2].startsWith('/'))
33
+ continue;
34
+ const f = join(repo, m[2]);
35
+ try {
36
+ const st = lstatSync(f);
37
+ if (!st.isFile())
38
+ continue;
39
+ if (createHash('sha256').update(readFileSync(f)).digest('hex') === m[1])
40
+ out.add(m[2]);
41
+ }
42
+ catch { /* not in the project */ }
43
+ }
44
+ return out;
45
+ }
46
+ // The folders that hold the skipped kit files, collapsed to three levels, for the list of what discovery left out.
47
+ export const kitFolders = (files) => [...new Set([...files].map((f) => dirname(f).split('/').slice(0, 3).join('/') + '/'))].sort();
@@ -0,0 +1,110 @@
1
+ // File listing for discovery. Reads the folder tree directly and applies .gitignore files itself, so discovery never runs
2
+ // git (a repository's git config can make git run code). Dependency, build output and tool folders are skipped and reported.
3
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { cmp, globBody } from '../util.js';
6
+ import { kitFolders } from './kit-files.js';
7
+ const SKIP_DIRS = {
8
+ node_modules: 'dependency folder', bower_components: 'dependency folder', jspm_packages: 'dependency folder',
9
+ vendor: 'dependency folder', '.venv': 'dependency folder', venv: 'dependency folder', Pods: 'dependency folder',
10
+ Carthage: 'dependency folder', '.bundle': 'dependency folder', '.pnpm-store': 'dependency folder', '.yarn': 'dependency folder',
11
+ dist: 'build output', build: 'build output', out: 'build output', '.next': 'build output', '.nuxt': 'build output',
12
+ '.svelte-kit': 'build output', '.output': 'build output', '.vercel': 'build output', '.netlify': 'build output',
13
+ target: 'build output', '.gradle': 'build output', DerivedData: 'build output', coverage: 'build output',
14
+ '__pycache__': 'build output', '.pytest_cache': 'build output', '.mypy_cache': 'build output', '.tox': 'build output',
15
+ '.dart_tool': 'build output', '.expo': 'build output', '.turbo': 'build output', '.cache': 'build output',
16
+ '.parcel-cache': 'build output', '.terraform': 'build output', '.serverless': 'build output', '.aws-sam': 'build output',
17
+ 'cdk.out': 'build output',
18
+ '.git': 'tool folder', '.hg': 'tool folder', '.svn': 'tool folder', '.hullproof': 'tool folder', '.idea': 'tool folder',
19
+ };
20
+ export function parseGitignore(text, base) {
21
+ const rules = [];
22
+ for (const raw of text.split(/\r?\n/)) {
23
+ let p = raw.replace(/(?<!\\)\s+$/, '');
24
+ if (!p || p.startsWith('#'))
25
+ continue;
26
+ const negate = p.startsWith('!');
27
+ if (negate)
28
+ p = p.slice(1);
29
+ if (p.startsWith('\\'))
30
+ p = p.slice(1);
31
+ const dirOnly = p.endsWith('/');
32
+ if (dirOnly)
33
+ p = p.slice(0, -1);
34
+ if (!p)
35
+ continue;
36
+ const anchored = p.includes('/');
37
+ if (p.startsWith('/'))
38
+ p = p.slice(1);
39
+ const body = globBody(p); // ponytail: also reads {a,b} as alternation, which gitignore does not; rare in practice
40
+ rules.push({ base, negate, dirOnly, re: new RegExp(anchored ? `^${body}$` : `^(?:.*/)?${body}$`) });
41
+ }
42
+ return rules;
43
+ }
44
+ function ignored(rel, isDir, rules) {
45
+ let out = false;
46
+ for (const r of rules) {
47
+ if (r.dirOnly && !isDir)
48
+ continue;
49
+ let sub;
50
+ if (!r.base)
51
+ sub = rel;
52
+ else if (rel.startsWith(r.base + '/'))
53
+ sub = rel.slice(r.base.length + 1);
54
+ else
55
+ continue;
56
+ if (r.re.test(sub))
57
+ out = !r.negate;
58
+ }
59
+ return out;
60
+ }
61
+ export function walk(root, maxFiles, skipFiles) {
62
+ const files = [];
63
+ const skipped = [];
64
+ let capReached = false;
65
+ const absRoot = resolve(root);
66
+ const visit = (dirRel, inherited) => {
67
+ const abs = dirRel ? join(absRoot, dirRel) : absRoot;
68
+ let rules = inherited;
69
+ try {
70
+ rules = inherited.concat(parseGitignore(readFileSync(join(abs, '.gitignore'), 'utf8'), dirRel));
71
+ }
72
+ catch { /* no .gitignore here */ }
73
+ const entries = readdirSync(abs, { withFileTypes: true }).sort((a, b) => cmp(a.name, b.name));
74
+ for (const e of entries) {
75
+ if (capReached)
76
+ return;
77
+ const rel = dirRel ? `${dirRel}/${e.name}` : e.name;
78
+ if (e.isSymbolicLink()) {
79
+ skipped.push({ path: rel, reason: 'symlink' });
80
+ continue;
81
+ }
82
+ const isDir = e.isDirectory();
83
+ if (!isDir && !e.isFile())
84
+ continue;
85
+ if (isDir && SKIP_DIRS[e.name]) {
86
+ skipped.push({ path: rel + '/', reason: SKIP_DIRS[e.name] });
87
+ continue;
88
+ }
89
+ if (ignored(rel, isDir, rules)) {
90
+ skipped.push({ path: isDir ? rel + '/' : rel, reason: 'gitignore' });
91
+ continue;
92
+ }
93
+ if (isDir)
94
+ visit(rel, rules);
95
+ else if (skipFiles?.has(rel))
96
+ continue;
97
+ else if (files.length >= maxFiles)
98
+ capReached = true;
99
+ else
100
+ files.push(rel);
101
+ }
102
+ };
103
+ if (!statSync(absRoot).isDirectory())
104
+ throw new Error(`not a folder: ${root}`);
105
+ visit('', []);
106
+ if (skipFiles?.size)
107
+ for (const d of kitFolders(skipFiles))
108
+ skipped.push({ path: d, reason: 'unchanged kit files' });
109
+ return { files, skipped, capReached };
110
+ }
package/dist/env.js ADDED
@@ -0,0 +1,57 @@
1
+ // Reads provider settings from a .env.local file without any dependency. Real environment variables always win. Only the names
2
+ // in HUNT_ENV_NAMES are read, and no value is ever logged or returned in a message: callers get the names that came from a file.
3
+ import { existsSync, readFileSync, statSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ export const HUNT_ENV_NAMES = [
7
+ 'HULLPROOF_PROVIDER', 'HULLPROOF_MODEL', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY',
8
+ 'HULLPROOF_OPENAI_COMPAT_BASE_URL', 'HULLPROOF_OPENAI_COMPAT_KEY',
9
+ ];
10
+ const ALLOWED = new Set(HUNT_ENV_NAMES);
11
+ const ENGINE_ROOT = fileURLToPath(new URL('../', import.meta.url));
12
+ const MAX_BYTES = 64 * 1024;
13
+ // Parses KEY=VALUE lines: blank lines and # comments skipped, an optional "export " prefix, matching quotes removed.
14
+ export function parseEnvText(text) {
15
+ const out = {};
16
+ for (const raw of text.split(/\r?\n/)) {
17
+ const line = raw.trim();
18
+ if (!line || line.startsWith('#'))
19
+ continue;
20
+ const m = /^(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line);
21
+ if (!m || !ALLOWED.has(m[1]))
22
+ continue;
23
+ let v = m[2].trim();
24
+ const q = v[0] === '"' || v[0] === "'" ? v.indexOf(v[0], 1) : -1;
25
+ if (q > 0)
26
+ v = v.slice(1, q);
27
+ else
28
+ v = v.replace(/\s+#.*$/, '');
29
+ if (v)
30
+ out[m[1]] = v;
31
+ }
32
+ return out;
33
+ }
34
+ // The scanned repository's .env.local first, then the engine's. A name already set (and not empty) in `base` is never replaced.
35
+ export function loadHuntEnv(repo, base = process.env, engineRoot = ENGINE_ROOT) {
36
+ const env = { ...base };
37
+ const fromFile = [];
38
+ // CI sets this: the scanned repository is untrusted there, so a .env.local inside it must not supply a provider address or key.
39
+ if (base.HULLPROOF_NO_ENV_FILE === '1')
40
+ return { env, fromFile };
41
+ const roots = [...(repo ? [resolve(repo)] : []), resolve(engineRoot)];
42
+ for (const root of roots) {
43
+ const p = join(root, '.env.local');
44
+ try {
45
+ if (!existsSync(p) || statSync(p).size > MAX_BYTES)
46
+ continue;
47
+ for (const [k, v] of Object.entries(parseEnvText(readFileSync(p, 'utf8')))) {
48
+ if (env[k])
49
+ continue;
50
+ env[k] = v;
51
+ fromFile.push(k);
52
+ }
53
+ }
54
+ catch { /* an unreadable file is the same as no file */ }
55
+ }
56
+ return { env, fromFile: [...new Set(fromFile)].sort() };
57
+ }