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,157 @@
1
+ // A strict structural check of the SARIF 2.1.0 file the engine writes. The official JSON Schema of SARIF 2.1.0 is not shipped with
2
+ // the engine and was not available offline when this was written, so this checks the parts of that schema the engine uses: required
3
+ // properties, types, enum values, array minimums, the allowed property names of every object the engine writes, and the links
4
+ // between a result and its rule. It does not claim to be the whole schema. GitHub code scanning applies the real schema on upload.
5
+ const LEVELS = new Set(['none', 'note', 'warning', 'error']);
6
+ const OBJ = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
7
+ // The property names the engine may write in each object, taken from the SARIF 2.1.0 definitions of those objects.
8
+ const ALLOWED = {
9
+ log: ['$schema', 'version', 'runs'],
10
+ run: ['tool', 'results', 'automationDetails', 'columnKind'],
11
+ driver: ['name', 'version', 'semanticVersion', 'rules', 'informationUri'],
12
+ rule: ['id', 'name', 'shortDescription', 'fullDescription', 'defaultConfiguration', 'properties'],
13
+ result: ['ruleId', 'ruleIndex', 'level', 'message', 'locations', 'partialFingerprints', 'properties'],
14
+ location: ['physicalLocation', 'message'],
15
+ physical: ['artifactLocation', 'region'],
16
+ artifact: ['uri'],
17
+ region: ['startLine'],
18
+ };
19
+ export function checkSarif(doc) {
20
+ const errs = [];
21
+ const e = (path, why) => { errs.push(`${path}: ${why}`); };
22
+ const only = (v, kind, path) => {
23
+ for (const k of Object.keys(v))
24
+ if (!ALLOWED[kind].includes(k))
25
+ e(path, `property "${k}" is not one the engine writes`);
26
+ };
27
+ const text = (v, path) => {
28
+ if (!OBJ(v) || typeof v.text !== 'string' || v.text.length === 0)
29
+ e(path, 'needs a non empty "text"');
30
+ };
31
+ if (!OBJ(doc))
32
+ return ['$: not an object'];
33
+ only(doc, 'log', '$');
34
+ if (doc.version !== '2.1.0')
35
+ e('$.version', 'must be "2.1.0"');
36
+ if (typeof doc.$schema !== 'string' || !doc.$schema.includes('sarif-2.1.0'))
37
+ e('$.$schema', 'must name the 2.1.0 schema');
38
+ if (!Array.isArray(doc.runs) || doc.runs.length < 1) {
39
+ e('$.runs', 'needs at least one run');
40
+ return errs;
41
+ }
42
+ doc.runs.forEach((run, ri) => {
43
+ const rp = `$.runs[${ri}]`;
44
+ if (!OBJ(run)) {
45
+ e(rp, 'not an object');
46
+ return;
47
+ }
48
+ only(run, 'run', rp);
49
+ const tool = run.tool;
50
+ if (!OBJ(tool) || !OBJ(tool.driver)) {
51
+ e(`${rp}.tool.driver`, 'required');
52
+ return;
53
+ }
54
+ const d = tool.driver;
55
+ only(d, 'driver', `${rp}.tool.driver`);
56
+ if (typeof d.name !== 'string' || !d.name)
57
+ e(`${rp}.tool.driver.name`, 'required');
58
+ const rules = d.rules === undefined ? [] : d.rules;
59
+ if (!Array.isArray(rules))
60
+ e(`${rp}.tool.driver.rules`, 'must be an array');
61
+ const ruleIds = [];
62
+ (Array.isArray(rules) ? rules : []).forEach((r, i) => {
63
+ const p = `${rp}.tool.driver.rules[${i}]`;
64
+ if (!OBJ(r)) {
65
+ e(p, 'not an object');
66
+ return;
67
+ }
68
+ only(r, 'rule', p);
69
+ if (typeof r.id !== 'string' || !r.id)
70
+ e(`${p}.id`, 'required');
71
+ else {
72
+ if (ruleIds.includes(r.id))
73
+ e(`${p}.id`, 'duplicate rule id');
74
+ ruleIds.push(r.id);
75
+ }
76
+ if (r.shortDescription !== undefined)
77
+ text(r.shortDescription, `${p}.shortDescription`);
78
+ if (r.fullDescription !== undefined)
79
+ text(r.fullDescription, `${p}.fullDescription`);
80
+ if (OBJ(r.defaultConfiguration) && r.defaultConfiguration.level !== undefined && !LEVELS.has(String(r.defaultConfiguration.level)))
81
+ e(`${p}.defaultConfiguration.level`, 'not a SARIF level');
82
+ const sev = OBJ(r.properties) ? r.properties['security-severity'] : undefined;
83
+ if (sev !== undefined && !(typeof sev === 'string' && /^\d+(\.\d+)?$/.test(sev) && Number(sev) <= 10))
84
+ e(`${p}.properties.security-severity`, 'must be a string number from 0 to 10');
85
+ });
86
+ if (!Array.isArray(run.results)) {
87
+ e(`${rp}.results`, 'must be an array');
88
+ return;
89
+ }
90
+ run.results.forEach((res, i) => {
91
+ const p = `${rp}.results[${i}]`;
92
+ if (!OBJ(res)) {
93
+ e(p, 'not an object');
94
+ return;
95
+ }
96
+ only(res, 'result', p);
97
+ text(res.message, `${p}.message`);
98
+ if (res.level !== undefined && !LEVELS.has(String(res.level)))
99
+ e(`${p}.level`, 'not a SARIF level');
100
+ if (typeof res.ruleId !== 'string' || !res.ruleId)
101
+ e(`${p}.ruleId`, 'required');
102
+ if (res.ruleIndex !== undefined) {
103
+ if (!Number.isInteger(res.ruleIndex) || res.ruleIndex < 0 || res.ruleIndex >= ruleIds.length)
104
+ e(`${p}.ruleIndex`, 'not an index into tool.driver.rules');
105
+ else if (ruleIds[res.ruleIndex] !== res.ruleId)
106
+ e(`${p}.ruleIndex`, 'points at a rule with another id');
107
+ }
108
+ else if (!ruleIds.includes(String(res.ruleId)))
109
+ e(`${p}.ruleId`, 'names no rule in tool.driver.rules');
110
+ if (!Array.isArray(res.locations) || res.locations.length < 1)
111
+ e(`${p}.locations`, 'needs at least one location');
112
+ else
113
+ res.locations.forEach((l, li) => {
114
+ const lp = `${p}.locations[${li}]`;
115
+ if (!OBJ(l)) {
116
+ e(lp, 'not an object');
117
+ return;
118
+ }
119
+ only(l, 'location', lp);
120
+ if (l.message !== undefined)
121
+ text(l.message, `${lp}.message`);
122
+ const ph = l.physicalLocation;
123
+ if (!OBJ(ph)) {
124
+ e(`${lp}.physicalLocation`, 'required');
125
+ return;
126
+ }
127
+ only(ph, 'physical', `${lp}.physicalLocation`);
128
+ const al = ph.artifactLocation;
129
+ if (!OBJ(al))
130
+ e(`${lp}.physicalLocation.artifactLocation`, 'required');
131
+ else {
132
+ only(al, 'artifact', `${lp}.physicalLocation.artifactLocation`);
133
+ const u = al.uri;
134
+ if (typeof u !== 'string' || !u)
135
+ e(`${lp}.physicalLocation.artifactLocation.uri`, 'required');
136
+ else if (/^[a-z][a-z0-9+.-]*:|^\/|(^|\/)\.\.(\/|$)|\\/i.test(u))
137
+ e(`${lp}.physicalLocation.artifactLocation.uri`, 'must be a relative path inside the repository');
138
+ }
139
+ const rg = ph.region;
140
+ if (rg !== undefined) {
141
+ if (!OBJ(rg))
142
+ e(`${lp}.physicalLocation.region`, 'not an object');
143
+ else {
144
+ only(rg, 'region', `${lp}.physicalLocation.region`);
145
+ if (!Number.isInteger(rg.startLine) || rg.startLine < 1)
146
+ e(`${lp}.physicalLocation.region.startLine`, 'must be an integer of 1 or more');
147
+ }
148
+ }
149
+ });
150
+ if (res.partialFingerprints !== undefined) {
151
+ if (!OBJ(res.partialFingerprints) || Object.values(res.partialFingerprints).some((x) => typeof x !== 'string' || !x))
152
+ e(`${p}.partialFingerprints`, 'must map names to non empty strings');
153
+ }
154
+ });
155
+ });
156
+ return errs;
157
+ }
@@ -0,0 +1,61 @@
1
+ // results.sarif: the fail records of the findings file as SARIF 2.1.0, for GitHub code scanning and other viewers.
2
+ // Same findings, same bytes: no clock, no absolute path, everything sorted. Only fail records are results. Observations, leads and
3
+ // routed items stay out, as they stay out of the gate.
4
+ import { cmp, stableJson } from '../util.js';
5
+ import { ENGINE_VERSION } from '../version.js';
6
+ export const SARIF_VERSION = '2.1.0';
7
+ export const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
8
+ // GitHub reads the security-severity property as a score: above 9 critical, 7 to 8.9 high, 4 to 6.9 medium, below 4 low.
9
+ const SCORE = { BLOCKER: '9.8', CRITICAL: '9.1', HIGH: '7.5', MEDIUM: '5.0', LOW: '2.0' };
10
+ const LEVEL = { BLOCKER: 'error', CRITICAL: 'error', HIGH: 'warning', MEDIUM: 'warning', LOW: 'note' };
11
+ // A repository path as a relative URI: no leading slash or dot segment, percent encoded where a URI needs it.
12
+ export const uriOf = (file) => file.replace(/\\/g, '/').replace(/^(\.\/)+/, '').replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
13
+ export function buildSarif(findings, kit, fallbackFile) {
14
+ const fails = findings.records.filter((r) => r.kind === 'fail').sort((a, b) => cmp(a.fingerprint, b.fingerprint));
15
+ // One SARIF rule per requirement id that appears in a fail record, in id order.
16
+ const ids = [...new Set(fails.flatMap((f) => f.sec_ids))].sort(cmp);
17
+ const rules = ids.map((id) => {
18
+ const c = kit.byId.get(id);
19
+ return {
20
+ id,
21
+ name: id.replace(/[^A-Za-z0-9]/g, ''),
22
+ shortDescription: { text: c?.title ?? id },
23
+ fullDescription: { text: c ? `${c.title}. Severity ${c.severity}, ${c.authority} authority, ${c.edition} edition.` : id },
24
+ defaultConfiguration: { level: LEVEL[c?.severity ?? 'HIGH'] },
25
+ properties: { 'security-severity': SCORE[c?.severity ?? 'HIGH'], tags: ['security', c?.severity ?? 'HIGH'] },
26
+ };
27
+ });
28
+ const ruleIndex = new Map(ids.map((id, i) => [id, i]));
29
+ const results = fails.map((f) => {
30
+ const primary = f.sec_ids[0];
31
+ const ev = f.evidence.length ? f.evidence : [{ file: fallbackFile, line: 1, note: '' }];
32
+ const locations = ev.slice(0, 8).map((e) => ({
33
+ physicalLocation: { artifactLocation: { uri: uriOf(e.file) }, region: { startLine: Math.max(1, e.line) } },
34
+ ...(e.note ? { message: { text: e.note } } : {}),
35
+ }));
36
+ return {
37
+ ruleId: primary,
38
+ ruleIndex: ruleIndex.get(primary),
39
+ level: LEVEL[f.severity],
40
+ message: { text: `${f.severity}: ${f.title}. ${f.impact_statement} How to fix it: ${f.remediation.strategy}` },
41
+ locations,
42
+ partialFingerprints: { 'hullproofFingerprint/v1': f.fingerprint },
43
+ properties: {
44
+ severity: f.severity, confidence: f.confidence, requirements: f.sec_ids, tool: f.machine_evidence?.tool ?? 'unknown',
45
+ 'security-severity': SCORE[f.severity],
46
+ },
47
+ };
48
+ });
49
+ return {
50
+ $schema: SARIF_SCHEMA,
51
+ version: SARIF_VERSION,
52
+ runs: [{
53
+ tool: { driver: { name: 'Hullproof Engine', version: ENGINE_VERSION, semanticVersion: ENGINE_VERSION, rules } },
54
+ // Paths are relative to the repository root. The category keeps one code scanning history for the engine.
55
+ automationDetails: { id: 'hullproof/' },
56
+ columnKind: 'utf16CodeUnits',
57
+ results,
58
+ }],
59
+ };
60
+ }
61
+ export const sarifJson = (s) => stableJson(s);
package/dist/cli.js ADDED
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ // hullproof: the engine command line. Node 22.18 or later runs this TypeScript file directly. No dependencies.
3
+ import { mkdirSync, realpathSync, writeFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { discover } from './discover/discover.js';
7
+ import { mapCommand } from './commands/map.js';
8
+ import { scan } from './commands/scan.js';
9
+ import { huntCommand } from './commands/hunt.js';
10
+ import { ciCommand } from './commands/ci.js';
11
+ import { hooksCommand } from './commands/hooks.js';
12
+ import { mcpCommand } from './commands/mcp.js';
13
+ import { FAIL_ON_LEVELS } from './ci/comment.js';
14
+ import { proveCommand } from './commands/prove.js';
15
+ import { packCommand } from './commands/pack.js';
16
+ import { gate, report } from './commands/report-gate.js';
17
+ import { stableJson } from './util.js';
18
+ import { ENGINE_VERSION } from './version.js';
19
+ // Exit codes. 0, 1 and 2 match the kit gate; 3 is reserved for a command that exists but is not built yet (none today).
20
+ export const EXIT = { ok: 0, open: 1, badInput: 2, notImplemented: 3 };
21
+ const HELP = `hullproof ${ENGINE_VERSION}
22
+
23
+ Usage: hullproof <command> [repository] [options]
24
+
25
+ Commands:
26
+ discover Detect the stack of a repository and write stack.json
27
+ map Build the code map of a repository (entry points and what protects them) and write codemap.json
28
+ scan Run discovery, the scanners and the checks; write <out>/<commit>/
29
+ ci One command for CI: scan, map, check, gate; write findings.json, report.md, results.sarif, pr-comment.md
30
+ hooks Install or remove the git pre-commit hook (install, uninstall), or run its fast check (run)
31
+ mcp Serve the scan results to an AI tool over MCP on stdio (read only; run_scan with --allow-run)
32
+ hunt Hunt for leads with a model, confirm them with evidence, or replay saved outputs (--replay)
33
+ prove Run test specs against a local copy of the app in a sandbox and keep the transcripts as evidence
34
+ pack Validate the stack packs (validate), list them (list), or print the coverage table from their recorded results (table)
35
+ report Write report.md from a scan folder
36
+ gate Apply the release gate to a scan folder and exit with its code
37
+ version Print the engine version
38
+ help Print this help
39
+
40
+ Options for discover:
41
+ --out DIR Folder for stack.json (default: .hullproof inside the repository). Use --out - to print to standard output.
42
+
43
+ Options for map:
44
+ --out DIR Folder for codemap.json (default: .hullproof inside the repository). Use --out - to print to standard output.
45
+
46
+ Options for scan, report and gate:
47
+ --out DIR Base folder (default: .hullproof inside the repository). Files go to DIR/<commit>/.
48
+ --kit DIR The kit folder holding docs/hullproof/security-controls.json (default: HULLPROOF_KIT, the
49
+ repository itself when the kit is installed there, then a folder called hullproof-free/kit next to the engine)
50
+ scan only:
51
+ --edition free|pro Which requirements are in scope (default pro)
52
+ --stage LAUNCH|GROWTH|SCALE Default: docs/security/STAGE.md, else LAUNCH
53
+ --allow-network Let OSV Scanner reach osv.dev (off by default)
54
+ --tools a,b Limit the scanners (semgrep, gitleaks, osv-scanner, trivy)
55
+ --rule-pack DIR Add the Semgrep rule files in DIR (for example a rule pack of your own) to the engine rules; a file with
56
+ the same name as an engine rule file replaces it (default: HULLPROOF_RULE_PACK, else none)
57
+ report and gate:
58
+ --commit SHA Read the scan folder of this commit (default: HEAD)
59
+ --owner FILE Owner evidence: audit_report, rollback, results rows, acceptances (see README)
60
+ --date YYYY-MM-DD The day acceptances are judged on (default: today, UTC)
61
+
62
+ Options for ci (also --out, --kit, --edition, --stage, --allow-network, --tools, --rule-pack as for scan; --owner, --date as for gate):
63
+ --fail-on LEVEL blocker, critical, high, medium, low or none (default blocker). Exit 1 when a finding at or above LEVEL failed.
64
+ --artifacts DIR Flat folder for the four output files (default: ci inside the --out folder)
65
+ --hunt Pro only: run the model hunt first. Needs a provider key in the environment (a CI secret); fork pull requests get none.
66
+ --provider, --model as for hunt
67
+ ci runs no model and no container unless --hunt is given. The same commit gives the same four files, byte for byte.
68
+
69
+ Options for hooks (hullproof hooks install|uninstall|run [repository]):
70
+ --mode git|husky|pre-commit Where the hook goes (default: Husky if the repository uses it, else the pre-commit framework if it has a config, else plain git)
71
+ --dry-run install and uninstall: say what would change and change nothing
72
+ --kit DIR run: the kit folder (as for scan)
73
+ --no-semgrep run: check secrets and the deterministic checks only (also: HULLPROOF_HOOK_SEMGREP=0)
74
+ run looks at the staged files only, blocks (exit 1) on a secret or a BLOCKER finding in them, and sends nothing anywhere. An error never blocks a commit.
75
+
76
+ Options for mcp (hullproof mcp [repository]):
77
+ --kit DIR The kit folder (as for scan)
78
+ --edition free|pro Which requirements are served (default: the kit decides)
79
+ --allow-run Also offer run_scan, which runs hullproof ci and writes only under .hullproof/. Off by default.
80
+ mcp makes no network call, runs no model, and never leaves the repository folder. It writes nothing but JSON-RPC to standard output.
81
+
82
+ Options for hunt:
83
+ --provider NAME anthropic, openai, gemini or local (default: HULLPROOF_PROVIDER, else the one provider whose key is set)
84
+ --model NAME The model to call (default: HULLPROOF_MODEL). Keys come from the environment or a .env.local file, never from a flag.
85
+ --runs N Hunting runs per entry point (default 3), then one synthesis call
86
+ --dry-run List the entry points and calls and make no model call
87
+ --resume Continue an interrupted hunt from the answers already saved
88
+ --replay Rerun the confirmation from the saved answers; no model, same bytes every time
89
+ --out, --kit, --commit as for report
90
+
91
+ Options for prove:
92
+ --allow-run Required to run the app. The owner must also type the confirmation (asked on a terminal, or pass --confirm).
93
+ --confirm TEXT The confirmation phrase, for a run with no terminal. Nothing else (no variable, no CI setting) can stand in for it.
94
+ --spec FILE[,FILE] Test specs written by hand (see docs/INTERFACES.md); they are checked and copied into the scan folder.
95
+ --allow-install-scripts Let dependency install scripts run in the build container (default off). Needed for native modules such
96
+ as better-sqlite3, bcrypt and sharp. Third party code runs with network access to the package registry.
97
+ --gen Write specs from hunt leads with a model (Pro). Runs nothing unless --allow-run is also given.
98
+ --replay Rebuild result.json from the saved specs and transcripts. No sandbox, no app, same bytes every time.
99
+ --self-test Run the escape attempts (network call, host file read, fork bomb, long loop) against this machine's container runtime.
100
+ --runtime NAME The container command: docker (Colima and Docker Desktop) or podman (default: HULLPROOF_CONTAINER_BIN, else docker)
101
+ --provider, --model, --out, --commit as for hunt
102
+ prove accepts no address. It only ever runs a copy of the app inside the sandbox, and refuses --target.
103
+
104
+ Options for pack (hullproof pack validate|list|table [pack folder ...]):
105
+ --root DIR The engine checkout the packs name their files in (default: this engine). Folders given by name are validated in place.
106
+ validate rejects a pack that is missing a part for its level, lists a rule without a test, has no fixture result, uses a bad level or claims
107
+ a level its recorded fixture result does not support. It reads files only. Exit 1 when a pack fails.
108
+
109
+ Exit codes: 0 done (gate: READY or READY WITH ACCEPTED RISK), 1 open items (gate: NOT READY),
110
+ 2 bad input, a refused run or a records problem.
111
+ `;
112
+ const TOOLS = ['semgrep', 'osv-scanner', 'gitleaks', 'trivy', 'checkov'];
113
+ const VALUE_FLAGS = new Set(['--mode', '--fail-on', '--artifacts', '--provider', '--model', '--runs', '--out', '--kit', '--edition', '--stage', '--tools', '--commit', '--owner', '--date', '--rule-pack', '--confirm', '--spec', '--runtime', '--target']);
114
+ const BOOL_FLAGS = new Set(['--no-semgrep', '--hunt', '--allow-network', '--dry-run', '--resume', '--replay', '--allow-run', '--gen', '--self-test', '--allow-install-scripts']);
115
+ function parse(cmd, rest, allowed) {
116
+ const flags = new Map();
117
+ let repo = '.';
118
+ for (let i = 0; i < rest.length; i++) {
119
+ const a = rest[i];
120
+ if (!a.startsWith('-')) {
121
+ repo = a;
122
+ continue;
123
+ }
124
+ if (!allowed.includes(a))
125
+ return `unknown option ${a}`;
126
+ if (BOOL_FLAGS.has(a)) {
127
+ flags.set(a, true);
128
+ continue;
129
+ }
130
+ const v = rest[++i];
131
+ if (v === undefined || (VALUE_FLAGS.has(a) && v.startsWith('--')))
132
+ return `${a} needs a value`;
133
+ flags.set(a, v);
134
+ }
135
+ return { repo, flags };
136
+ }
137
+ function discoverCmd(rest, out, err) {
138
+ const p = parse('discover', rest, ['--out']);
139
+ if (typeof p === 'string') {
140
+ err(`hullproof discover: ${p}${p.startsWith('--out') ? ', or - for standard output' : ''}.\n`);
141
+ return EXIT.badInput;
142
+ }
143
+ const outDir = p.flags.get('--out') ?? null;
144
+ let json;
145
+ try {
146
+ json = stableJson(discover(p.repo));
147
+ }
148
+ catch (e) {
149
+ err(`hullproof discover: ${e.message}\n`);
150
+ return EXIT.badInput;
151
+ }
152
+ if (outDir === '-') {
153
+ out(json);
154
+ return EXIT.ok;
155
+ }
156
+ const dir = resolve(outDir ?? join(p.repo, '.hullproof'));
157
+ mkdirSync(dir, { recursive: true });
158
+ writeFileSync(join(dir, 'stack.json'), json);
159
+ out(`hullproof discover: wrote ${join(dir, 'stack.json')}\n`);
160
+ return EXIT.ok;
161
+ }
162
+ function mapCmd(rest, out, err) {
163
+ const p = parse('map', rest, ['--out']);
164
+ if (typeof p === 'string') {
165
+ err(`hullproof map: ${p}${p.startsWith('--out') ? ', or - for standard output' : ''}.\n`);
166
+ return EXIT.badInput;
167
+ }
168
+ return mapCommand(p.repo, p.flags.get('--out') ?? null, out, err);
169
+ }
170
+ async function scanCmd(rest, err) {
171
+ const p = parse('scan', rest, ['--out', '--kit', '--edition', '--stage', '--allow-network', '--tools', '--rule-pack']);
172
+ if (typeof p === 'string') {
173
+ err(`hullproof scan: ${p}\n`);
174
+ return EXIT.badInput;
175
+ }
176
+ const edition = p.flags.get('--edition') ?? 'pro';
177
+ const stage = p.flags.get('--stage') ?? null;
178
+ const tools = p.flags.has('--tools') ? String(p.flags.get('--tools')).split(',').map((t) => t.trim()) : null;
179
+ if (edition !== 'free' && edition !== 'pro') {
180
+ err('hullproof scan: --edition is free or pro\n');
181
+ return EXIT.badInput;
182
+ }
183
+ if (stage && !['LAUNCH', 'GROWTH', 'SCALE'].includes(stage)) {
184
+ err('hullproof scan: --stage is LAUNCH, GROWTH or SCALE\n');
185
+ return EXIT.badInput;
186
+ }
187
+ if (tools && tools.some((t) => !TOOLS.includes(t))) {
188
+ err(`hullproof scan: --tools takes ${TOOLS.join(', ')}\n`);
189
+ return EXIT.badInput;
190
+ }
191
+ return scan({
192
+ repo: p.repo, out: p.flags.get('--out') ?? null, kit: p.flags.get('--kit') ?? null,
193
+ edition, stage: stage, allowNetwork: p.flags.has('--allow-network'), tools: tools,
194
+ rulePack: p.flags.get('--rule-pack') ?? null, log: (s) => err(s + '\n'),
195
+ });
196
+ }
197
+ async function ciCmd(rest, out, err) {
198
+ const p = parse('ci', rest, ['--out', '--kit', '--edition', '--stage', '--allow-network', '--tools', '--rule-pack', '--owner', '--date', '--fail-on', '--artifacts', '--hunt', '--provider', '--model']);
199
+ if (typeof p === 'string') {
200
+ err(`hullproof ci: ${p}\n`);
201
+ return EXIT.badInput;
202
+ }
203
+ const get = (f) => p.flags.get(f) ?? null;
204
+ const edition = get('--edition');
205
+ const stage = get('--stage');
206
+ const failOn = (get('--fail-on') ?? 'blocker').toLowerCase();
207
+ const tools = get('--tools') ? String(get('--tools')).split(',').map((t) => t.trim()) : null;
208
+ if (edition && edition !== 'free' && edition !== 'pro') {
209
+ err('hullproof ci: --edition is free or pro\n');
210
+ return EXIT.badInput;
211
+ }
212
+ if (stage && !['LAUNCH', 'GROWTH', 'SCALE'].includes(stage)) {
213
+ err('hullproof ci: --stage is LAUNCH, GROWTH or SCALE\n');
214
+ return EXIT.badInput;
215
+ }
216
+ if (!FAIL_ON_LEVELS.includes(failOn)) {
217
+ err(`hullproof ci: --fail-on takes ${FAIL_ON_LEVELS.join(', ')}\n`);
218
+ return EXIT.badInput;
219
+ }
220
+ if (tools && tools.some((t) => !TOOLS.includes(t))) {
221
+ err(`hullproof ci: --tools takes ${TOOLS.join(', ')}\n`);
222
+ return EXIT.badInput;
223
+ }
224
+ if (get('--date') && !/^\d{4}-\d{2}-\d{2}$/.test(get('--date'))) {
225
+ err('hullproof ci: --date needs YYYY-MM-DD\n');
226
+ return EXIT.badInput;
227
+ }
228
+ return ciCommand({
229
+ repo: p.repo, out: get('--out'), kit: get('--kit'), edition: edition, stage: stage,
230
+ failOn: failOn, artifacts: get('--artifacts'), allowNetwork: p.flags.has('--allow-network'), tools: tools,
231
+ rulePack: get('--rule-pack'), owner: get('--owner'), date: get('--date'), hunt: p.flags.has('--hunt'), provider: get('--provider'), model: get('--model'),
232
+ log: (s) => err(s + '\n'), print: (s) => out(s + '\n'),
233
+ });
234
+ }
235
+ async function hooksCmd(rest, err) {
236
+ const [action, ...more] = rest;
237
+ const p = parse('hooks', more, ['--mode', '--dry-run', '--kit', '--no-semgrep']);
238
+ if (typeof p === 'string') {
239
+ err(`hullproof hooks: ${p}\n`);
240
+ return EXIT.badInput;
241
+ }
242
+ const get = (f) => p.flags.get(f) ?? null;
243
+ return hooksCommand({ action: action ?? '', repo: p.repo, mode: get('--mode'), dryRun: p.flags.has('--dry-run'), kit: get('--kit'), noSemgrep: p.flags.has('--no-semgrep'), log: (s) => err(s + '\n') });
244
+ }
245
+ async function mcpCmd(rest, err) {
246
+ const p = parse('mcp', rest, ['--kit', '--edition', '--allow-run']);
247
+ if (typeof p === 'string') {
248
+ err(`hullproof mcp: ${p}\n`);
249
+ return EXIT.badInput;
250
+ }
251
+ const get = (f) => p.flags.get(f) ?? null;
252
+ const edition = get('--edition');
253
+ if (edition && edition !== 'free' && edition !== 'pro') {
254
+ err('hullproof mcp: --edition is free or pro\n');
255
+ return EXIT.badInput;
256
+ }
257
+ return mcpCommand({ repo: p.repo, kit: get('--kit'), edition: edition, allowRun: p.flags.has('--allow-run'), log: (s) => err(s + '\n') });
258
+ }
259
+ async function readCmd(cmd, rest, err) {
260
+ const p = parse(cmd, rest, ['--out', '--kit', '--commit', '--owner', '--date']);
261
+ if (typeof p === 'string') {
262
+ err(`hullproof ${cmd}: ${p}\n`);
263
+ return EXIT.badInput;
264
+ }
265
+ const get = (f) => p.flags.get(f) ?? null;
266
+ const o = { repo: p.repo, out: get('--out'), kit: get('--kit'), commit: get('--commit'), owner: get('--owner'), date: get('--date'), log: (s) => err(s + '\n') };
267
+ return cmd === 'report' ? report(o) : gate(o);
268
+ }
269
+ async function huntCmd(rest, err) {
270
+ const p = parse('hunt', rest, ['--out', '--kit', '--commit', '--provider', '--model', '--runs', '--dry-run', '--resume', '--replay']);
271
+ if (typeof p === 'string') {
272
+ err(`hullproof hunt: ${p}\n`);
273
+ return EXIT.badInput;
274
+ }
275
+ const get = (f) => p.flags.get(f) ?? null;
276
+ const runsRaw = get('--runs');
277
+ if (runsRaw !== null && !/^[1-9]\d*$/.test(runsRaw)) {
278
+ err('hullproof hunt: --runs needs a whole number of 1 or more\n');
279
+ return EXIT.badInput;
280
+ }
281
+ return huntCommand({ repo: p.repo, out: get('--out'), kit: get('--kit'), commit: get('--commit'), provider: get('--provider'), model: get('--model'),
282
+ runs: runsRaw === null ? null : Number(runsRaw), dryRun: p.flags.has('--dry-run'), resume: p.flags.has('--resume'), replay: p.flags.has('--replay'), log: (s) => err(s + '\n') });
283
+ }
284
+ function packCmd(rest, out, err) {
285
+ const [action, ...more] = rest;
286
+ const targets = [];
287
+ let root = null;
288
+ for (let i = 0; i < more.length; i++) {
289
+ if (more[i] === '--root') {
290
+ root = more[++i] ?? null;
291
+ if (!root) {
292
+ err('hullproof pack: --root needs a folder.\n');
293
+ return EXIT.badInput;
294
+ }
295
+ }
296
+ else if (more[i].startsWith('-')) {
297
+ err(`hullproof pack: unknown option ${more[i]}.\n`);
298
+ return EXIT.badInput;
299
+ }
300
+ else
301
+ targets.push(more[i]);
302
+ }
303
+ return packCommand({ action: action ?? '', targets, root, out, err });
304
+ }
305
+ async function proveCmd(rest, err) {
306
+ const p = parse('prove', rest, ['--out', '--commit', '--allow-run', '--confirm', '--target', '--spec', '--allow-install-scripts', '--gen', '--provider', '--model', '--replay', '--self-test', '--runtime']);
307
+ if (typeof p === 'string') {
308
+ err(`hullproof prove: ${p}\n`);
309
+ return EXIT.badInput;
310
+ }
311
+ const get = (f) => p.flags.get(f) ?? null;
312
+ return proveCommand({ repo: p.repo, out: get('--out'), commit: get('--commit'), allowRun: p.flags.has('--allow-run'), allowInstallScripts: p.flags.has('--allow-install-scripts'), confirm: get('--confirm'), target: get('--target'),
313
+ specs: (get('--spec') ?? '').split(',').map((x) => x.trim()).filter(Boolean), gen: p.flags.has('--gen'), provider: get('--provider'), model: get('--model'),
314
+ replay: p.flags.has('--replay'), selfTest: p.flags.has('--self-test'), runtime: get('--runtime'), log: (s) => err(s + '\n') });
315
+ }
316
+ export function main(argv) {
317
+ const [cmd, ...rest] = argv;
318
+ const out = (s) => { process.stdout.write(s); };
319
+ const err = (s) => { process.stderr.write(s); };
320
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
321
+ out(HELP);
322
+ return EXIT.ok;
323
+ }
324
+ if (cmd === 'version' || cmd === '--version') {
325
+ out(`${ENGINE_VERSION}\n`);
326
+ return EXIT.ok;
327
+ }
328
+ if (cmd === 'discover')
329
+ return discoverCmd(rest, out, err);
330
+ if (cmd === 'map')
331
+ return mapCmd(rest, out, err);
332
+ if (cmd === 'scan')
333
+ return scanCmd(rest, err);
334
+ if (cmd === 'ci')
335
+ return ciCmd(rest, out, err);
336
+ if (cmd === 'hooks')
337
+ return hooksCmd(rest, err);
338
+ if (cmd === 'mcp')
339
+ return mcpCmd(rest, err);
340
+ if (cmd === 'hunt')
341
+ return huntCmd(rest, err);
342
+ if (cmd === 'prove')
343
+ return proveCmd(rest, err);
344
+ if (cmd === 'pack')
345
+ return packCmd(rest, out, err);
346
+ if (cmd === 'report' || cmd === 'gate')
347
+ return readCmd(cmd, rest, err);
348
+ err(`hullproof: unknown command "${cmd}". Run hullproof help.\n`);
349
+ return EXIT.badInput;
350
+ }
351
+ const invokedAs = () => { try {
352
+ return pathToFileURL(realpathSync(process.argv[1] ?? '')).href;
353
+ }
354
+ catch {
355
+ return '';
356
+ } };
357
+ if (import.meta.url === invokedAs())
358
+ process.exitCode = await main(process.argv.slice(2));