@clear-capabilities/agentic-security-scanner 0.74.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 (331) hide show
  1. package/CHANGELOG.md +1580 -0
  2. package/bin/.agentic-security/findings.json +1577 -0
  3. package/bin/.agentic-security/last-scan.json +1577 -0
  4. package/bin/.agentic-security/last-scan.json.sig +1 -0
  5. package/bin/.agentic-security/scan-history.json +465 -0
  6. package/bin/.agentic-security/streak.json +25 -0
  7. package/bin/agentic-security-audit.js +198 -0
  8. package/bin/agentic-security-consistency.js +80 -0
  9. package/bin/agentic-security-diff.js +136 -0
  10. package/bin/agentic-security-lsp.js +12 -0
  11. package/bin/agentic-security-mcp.js +40 -0
  12. package/bin/agentic-security-rule.js +153 -0
  13. package/bin/agentic-security.js +1683 -0
  14. package/dist/117.index.js +207 -0
  15. package/dist/178.index.js +250 -0
  16. package/dist/218.index.js +793 -0
  17. package/dist/227.index.js +192 -0
  18. package/dist/301.index.js +167 -0
  19. package/dist/384.index.js +18 -0
  20. package/dist/476.index.js +126 -0
  21. package/dist/513.index.js +373 -0
  22. package/dist/520.index.js +13 -0
  23. package/dist/601.index.js +1038 -0
  24. package/dist/634.index.js +1892 -0
  25. package/dist/637.index.js +216 -0
  26. package/dist/660.index.js +131 -0
  27. package/dist/675.index.js +451 -0
  28. package/dist/826.index.js +188 -0
  29. package/dist/830.index.js +133 -0
  30. package/dist/agentic-security.mjs +272 -0
  31. package/dist/agentic-security.mjs.sha256 +1 -0
  32. package/dist/calibration-seed.json +27 -0
  33. package/package.json +77 -0
  34. package/src/.agentic-security/findings.json +80844 -0
  35. package/src/.agentic-security/last-scan.json +80844 -0
  36. package/src/.agentic-security/last-scan.json.sig +1 -0
  37. package/src/.agentic-security/scan-history.json +8408 -0
  38. package/src/.agentic-security/streak.json +26 -0
  39. package/src/badge.js +188 -0
  40. package/src/compare.js +203 -0
  41. package/src/dataflow/.agentic-security/findings.json +3487 -0
  42. package/src/dataflow/.agentic-security/last-scan.json +3487 -0
  43. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  44. package/src/dataflow/.agentic-security/scan-history.json +735 -0
  45. package/src/dataflow/.agentic-security/streak.json +24 -0
  46. package/src/dataflow/CLAUDE.md +38 -0
  47. package/src/dataflow/access-paths.js +172 -0
  48. package/src/dataflow/async-sequencing.js +177 -0
  49. package/src/dataflow/backward.js +201 -0
  50. package/src/dataflow/catalog-expanded.js +485 -0
  51. package/src/dataflow/catalog.js +659 -0
  52. package/src/dataflow/cross-repo.js +219 -0
  53. package/src/dataflow/engine.js +588 -0
  54. package/src/dataflow/exception-flow.js +116 -0
  55. package/src/dataflow/exploit-prover.js +187 -0
  56. package/src/dataflow/higher-order.js +221 -0
  57. package/src/dataflow/ifds.js +347 -0
  58. package/src/dataflow/implicit-flow.js +129 -0
  59. package/src/dataflow/incremental.js +229 -0
  60. package/src/dataflow/index.js +181 -0
  61. package/src/dataflow/numeric-domain.js +192 -0
  62. package/src/dataflow/path-feasibility.js +114 -0
  63. package/src/dataflow/points-to.js +337 -0
  64. package/src/dataflow/polyglot.js +190 -0
  65. package/src/dataflow/proven-clean.js +159 -0
  66. package/src/dataflow/receiver-context.js +76 -0
  67. package/src/dataflow/sanitizer-proof.js +154 -0
  68. package/src/dataflow/soft-taint.js +140 -0
  69. package/src/dataflow/string-domain.js +234 -0
  70. package/src/dataflow/stub-aware-filter.js +100 -0
  71. package/src/dataflow/summaries.js +132 -0
  72. package/src/dataflow/symbolic-exec.js +238 -0
  73. package/src/dataflow/tabulation.js +135 -0
  74. package/src/engine.js +7763 -0
  75. package/src/history-scan.js +229 -0
  76. package/src/index.js +3 -0
  77. package/src/integrations/.agentic-security/findings.json +1504 -0
  78. package/src/integrations/.agentic-security/last-scan.json +1504 -0
  79. package/src/integrations/.agentic-security/scan-history.json +40 -0
  80. package/src/integrations/.agentic-security/streak.json +21 -0
  81. package/src/integrations/index.js +321 -0
  82. package/src/integrations/tickets.js +200 -0
  83. package/src/ir/.agentic-security/findings.json +3036 -0
  84. package/src/ir/.agentic-security/last-scan.json +3036 -0
  85. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  86. package/src/ir/.agentic-security/scan-history.json +364 -0
  87. package/src/ir/.agentic-security/streak.json +23 -0
  88. package/src/ir/CLAUDE.md +172 -0
  89. package/src/ir/callgraph.js +73 -0
  90. package/src/ir/class-hierarchy.js +195 -0
  91. package/src/ir/index.js +152 -0
  92. package/src/ir/parser-cs.js +260 -0
  93. package/src/ir/parser-java.js +286 -0
  94. package/src/ir/parser-js.js +413 -0
  95. package/src/ir/parser-kt.js +258 -0
  96. package/src/ir/parser-py-cst.js +136 -0
  97. package/src/ir/parser-py.helper.py +501 -0
  98. package/src/ir/parser-py.js +312 -0
  99. package/src/ir/ssa.js +315 -0
  100. package/src/ir/type-stubs.js +288 -0
  101. package/src/leaderboard.js +152 -0
  102. package/src/llm-validator/.agentic-security/findings.json +1891 -0
  103. package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
  104. package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
  105. package/src/llm-validator/.agentic-security/scan-history.json +168 -0
  106. package/src/llm-validator/.agentic-security/streak.json +20 -0
  107. package/src/llm-validator/consistency.js +141 -0
  108. package/src/llm-validator/index.js +437 -0
  109. package/src/lsp/.agentic-security/findings.json +28 -0
  110. package/src/lsp/.agentic-security/last-scan.json +28 -0
  111. package/src/lsp/.agentic-security/scan-history.json +79 -0
  112. package/src/lsp/.agentic-security/streak.json +22 -0
  113. package/src/lsp/server.js +275 -0
  114. package/src/mcp/.agentic-security/findings.json +8358 -0
  115. package/src/mcp/.agentic-security/last-scan.json +8358 -0
  116. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  117. package/src/mcp/.agentic-security/scan-history.json +1125 -0
  118. package/src/mcp/.agentic-security/streak.json +22 -0
  119. package/src/mcp/CLAUDE.md +54 -0
  120. package/src/mcp/audit.js +136 -0
  121. package/src/mcp/redact.js +75 -0
  122. package/src/mcp/server.js +158 -0
  123. package/src/mcp/stdio.js +83 -0
  124. package/src/mcp/tools.js +940 -0
  125. package/src/mcp/validate.js +49 -0
  126. package/src/personality.js +164 -0
  127. package/src/poc-video.js +239 -0
  128. package/src/posture/.agentic-security/findings.json +51239 -0
  129. package/src/posture/.agentic-security/last-scan.json +51239 -0
  130. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  131. package/src/posture/.agentic-security/scan-history.json +5557 -0
  132. package/src/posture/.agentic-security/streak.json +24 -0
  133. package/src/posture/CLAUDE.md +42 -0
  134. package/src/posture/adversarial-self-test.js +114 -0
  135. package/src/posture/adversary-agent.js +204 -0
  136. package/src/posture/agents-memory.js +135 -0
  137. package/src/posture/ai-code-fingerprint.js +171 -0
  138. package/src/posture/aibom.js +284 -0
  139. package/src/posture/api-inventory.js +96 -0
  140. package/src/posture/attack-playbooks.js +305 -0
  141. package/src/posture/auditor-agent.js +115 -0
  142. package/src/posture/auth-posture-import.js +135 -0
  143. package/src/posture/baseline-compare.js +114 -0
  144. package/src/posture/blast-radius.js +836 -0
  145. package/src/posture/bounty-prediction.js +141 -0
  146. package/src/posture/business-logic.js +239 -0
  147. package/src/posture/calibration-drift.js +93 -0
  148. package/src/posture/calibration-seed.json +27 -0
  149. package/src/posture/calibration.js +204 -0
  150. package/src/posture/clustering.js +75 -0
  151. package/src/posture/concurrency-checker.js +265 -0
  152. package/src/posture/confidence.js +65 -0
  153. package/src/posture/container-runtime.js +149 -0
  154. package/src/posture/counterfactual.js +109 -0
  155. package/src/posture/cross-lang-graphql.js +165 -0
  156. package/src/posture/cross-lang-grpc.js +166 -0
  157. package/src/posture/cross-lang-meta.js +101 -0
  158. package/src/posture/cross-lang-openapi.js +187 -0
  159. package/src/posture/cross-lang-orm.js +153 -0
  160. package/src/posture/cross-lang-queues.js +210 -0
  161. package/src/posture/crown-jewels.js +110 -0
  162. package/src/posture/custom-rules.js +361 -0
  163. package/src/posture/cve-alert-daemon.js +433 -0
  164. package/src/posture/cve-lookup.js +129 -0
  165. package/src/posture/dead-code.js +430 -0
  166. package/src/posture/defender-agent.js +158 -0
  167. package/src/posture/deploy-platform.js +204 -0
  168. package/src/posture/detector-fuzz.js +61 -0
  169. package/src/posture/deterministic.js +99 -0
  170. package/src/posture/drift.js +165 -0
  171. package/src/posture/epss.js +156 -0
  172. package/src/posture/exploitability-probability.js +212 -0
  173. package/src/posture/exploitability.js +121 -0
  174. package/src/posture/feature-flags.js +110 -0
  175. package/src/posture/finding-defaults.js +132 -0
  176. package/src/posture/fix-history.js +411 -0
  177. package/src/posture/fix-plan.js +121 -0
  178. package/src/posture/fix-verify-loop.js +157 -0
  179. package/src/posture/fix-verify.js +130 -0
  180. package/src/posture/flow-narration.js +105 -0
  181. package/src/posture/grader-calibration.js +156 -0
  182. package/src/posture/harness-discovery.js +113 -0
  183. package/src/posture/holdout-eval.js +144 -0
  184. package/src/posture/iac-reachability.js +163 -0
  185. package/src/posture/iam-policy.js +128 -0
  186. package/src/posture/integrity.js +97 -0
  187. package/src/posture/learning.js +166 -0
  188. package/src/posture/license-policy.js +109 -0
  189. package/src/posture/llm-redteam-prompts.js +418 -0
  190. package/src/posture/llm-redteam.js +303 -0
  191. package/src/posture/material-change.js +163 -0
  192. package/src/posture/mitigation-composite.js +55 -0
  193. package/src/posture/mttr.js +91 -0
  194. package/src/posture/network-policy-import.js +126 -0
  195. package/src/posture/path-predicates.js +99 -0
  196. package/src/posture/persona-prioritization.js +153 -0
  197. package/src/posture/poc-cwe-map.js +51 -0
  198. package/src/posture/poc-generator.js +500 -0
  199. package/src/posture/policy-gate.js +174 -0
  200. package/src/posture/pre-incident-archaeology.js +110 -0
  201. package/src/posture/profile.js +93 -0
  202. package/src/posture/reachability-filter.js +42 -0
  203. package/src/posture/regression-test-gen.js +200 -0
  204. package/src/posture/reverse-blast-radius.js +110 -0
  205. package/src/posture/router.js +109 -0
  206. package/src/posture/rule-overrides.js +198 -0
  207. package/src/posture/rule-pack-signing.js +209 -0
  208. package/src/posture/rule-packs.js +143 -0
  209. package/src/posture/rule-synthesis.js +108 -0
  210. package/src/posture/ruleset-version.js +71 -0
  211. package/src/posture/sbom.js +129 -0
  212. package/src/posture/schema-aware-bridge.js +207 -0
  213. package/src/posture/security-trend.js +87 -0
  214. package/src/posture/semantic-clone.js +114 -0
  215. package/src/posture/specification-mining.js +170 -0
  216. package/src/posture/stable-id.js +75 -0
  217. package/src/posture/stack-playbook.js +229 -0
  218. package/src/posture/streak.js +249 -0
  219. package/src/posture/suppressions.js +135 -0
  220. package/src/posture/telemetry-ingest.js +112 -0
  221. package/src/posture/threat-model.js +145 -0
  222. package/src/posture/three-agent-pipeline.js +74 -0
  223. package/src/posture/triage.js +146 -0
  224. package/src/posture/trust-boundary-diagram.js +115 -0
  225. package/src/posture/type-narrowing.js +129 -0
  226. package/src/posture/validator-metrics.js +179 -0
  227. package/src/posture/verifier-ephemeral.js +118 -0
  228. package/src/posture/verifier-target.js +147 -0
  229. package/src/posture/verifier.js +257 -0
  230. package/src/posture/version.js +75 -0
  231. package/src/posture/waf-ingest.js +200 -0
  232. package/src/posture/why-fired.js +141 -0
  233. package/src/pr-comment.js +172 -0
  234. package/src/pr-delta.js +198 -0
  235. package/src/report/.agentic-security/findings.json +79 -0
  236. package/src/report/.agentic-security/last-scan.json +79 -0
  237. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  238. package/src/report/.agentic-security/scan-history.json +332 -0
  239. package/src/report/.agentic-security/streak.json +23 -0
  240. package/src/report/index.js +1136 -0
  241. package/src/report/mascot.js +42 -0
  242. package/src/runScan.js +141 -0
  243. package/src/sast/.agentic-security/findings.json +5051 -0
  244. package/src/sast/.agentic-security/last-scan.json +5051 -0
  245. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  246. package/src/sast/.agentic-security/scan-history.json +788 -0
  247. package/src/sast/.agentic-security/streak.json +23 -0
  248. package/src/sast/CLAUDE.md +39 -0
  249. package/src/sast/_comment-strip.js +46 -0
  250. package/src/sast/agent-tool-escalation.js +131 -0
  251. package/src/sast/auth-provider.js +171 -0
  252. package/src/sast/authz.js +236 -0
  253. package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
  254. package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
  255. package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
  256. package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
  257. package/src/sast/bench-shape/index.js +62 -0
  258. package/src/sast/claude-hook-injection.js +199 -0
  259. package/src/sast/claude-md-prompt-injection.js +170 -0
  260. package/src/sast/claude-settings.js +165 -0
  261. package/src/sast/client-side.js +149 -0
  262. package/src/sast/cpp-bench-extras.js +122 -0
  263. package/src/sast/cpp-dataflow.js +430 -0
  264. package/src/sast/cpp.js +248 -0
  265. package/src/sast/csharp.js +152 -0
  266. package/src/sast/csrf.js +82 -0
  267. package/src/sast/dart-flutter.js +173 -0
  268. package/src/sast/db-rls.js +147 -0
  269. package/src/sast/db-taint.js +215 -0
  270. package/src/sast/defi-deep.js +242 -0
  271. package/src/sast/deserialization-gadgets.js +113 -0
  272. package/src/sast/django-hardening.js +230 -0
  273. package/src/sast/env-hygiene.js +125 -0
  274. package/src/sast/fastapi-hardening.js +145 -0
  275. package/src/sast/go-extended.js +84 -0
  276. package/src/sast/host-header.js +106 -0
  277. package/src/sast/index.js +17 -0
  278. package/src/sast/java-ast-folding.js +561 -0
  279. package/src/sast/java-bench-extras.js +708 -0
  280. package/src/sast/java-collection-passthrough.js +178 -0
  281. package/src/sast/java-constant-fold.js +244 -0
  282. package/src/sast/java-deserialization.js +125 -0
  283. package/src/sast/jndi.js +104 -0
  284. package/src/sast/juliet-shape.js +324 -0
  285. package/src/sast/jwt-exp.js +104 -0
  286. package/src/sast/kotlin.js +82 -0
  287. package/src/sast/laravel-hardening.js +198 -0
  288. package/src/sast/ldap-injection.js +100 -0
  289. package/src/sast/llm-owasp.js +465 -0
  290. package/src/sast/llm-stored-prompt.js +103 -0
  291. package/src/sast/llm-trading-agent.js +161 -0
  292. package/src/sast/llm.js +308 -0
  293. package/src/sast/logic.js +140 -0
  294. package/src/sast/mass-assignment.js +101 -0
  295. package/src/sast/mcp-audit.js +242 -0
  296. package/src/sast/mobile-manifest.js +195 -0
  297. package/src/sast/model-load.js +164 -0
  298. package/src/sast/mutation-xss.js +87 -0
  299. package/src/sast/nosql-injection.js +82 -0
  300. package/src/sast/open-redirect.js +119 -0
  301. package/src/sast/php.js +91 -0
  302. package/src/sast/pipeline.js +122 -0
  303. package/src/sast/primary-cwe-java.js +155 -0
  304. package/src/sast/prompt-firewall.js +151 -0
  305. package/src/sast/prompt-template.js +157 -0
  306. package/src/sast/prototype-pollution.js +112 -0
  307. package/src/sast/python-sinks.js +195 -0
  308. package/src/sast/quarkus-hardening.js +102 -0
  309. package/src/sast/rag-poisoning.js +118 -0
  310. package/src/sast/rate-limit.js +128 -0
  311. package/src/sast/response-splitting.js +138 -0
  312. package/src/sast/ruby.js +108 -0
  313. package/src/sast/rust.js +105 -0
  314. package/src/sast/solidity.js +167 -0
  315. package/src/sast/springboot-hardening.js +186 -0
  316. package/src/sast/ssrf-cloud-metadata.js +80 -0
  317. package/src/sast/ssti.js +116 -0
  318. package/src/sast/swift.js +162 -0
  319. package/src/sast/toctou.js +95 -0
  320. package/src/sast/webhook.js +101 -0
  321. package/src/sast/xpath-injection.js +51 -0
  322. package/src/sast/xxe.js +140 -0
  323. package/src/sast/zip-slip.js +200 -0
  324. package/src/sca/base-images.json +45 -0
  325. package/src/sca/container.js +107 -0
  326. package/src/sca/dep-confusion.js +134 -0
  327. package/src/sca/index.js +6 -0
  328. package/src/sca/popular-packages.json +41 -0
  329. package/src/sca/sarif-ingest.js +187 -0
  330. package/src/sca/vuln-function-hints.json +89 -0
  331. package/src/secrets/index.js +4 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "firstScanDate": "2026-05-13T13:02:38.907Z",
3
+ "lastScanDate": "2026-05-20T17:07:59.655Z",
4
+ "totalScans": 203,
5
+ "daysCleanCritical": 4,
6
+ "lastCleanDate": "2026-05-20",
7
+ "lastCriticalDate": null,
8
+ "hasEverHadCritical": false,
9
+ "bestDaysCleanCritical": 4,
10
+ "totalFindingsAtFirstScan": 11,
11
+ "totalFindingsAtLastScan": 27,
12
+ "totalFixesInferred": 1,
13
+ "lastGrade": "A-",
14
+ "bestGrade": "A-",
15
+ "launchCheckPassedAt": null,
16
+ "achievements": [
17
+ "first-fix",
18
+ "first-scan",
19
+ "scan-veteran-100",
20
+ "scan-veteran-25"
21
+ ],
22
+ "previousGrade": "A-"
23
+ }
@@ -0,0 +1,39 @@
1
+ # scanner/src/sast/
2
+
3
+ SAST detector modules. Each file exports one or more `scan*()` functions returning `Finding[]`. The orchestrator in `../engine.js` calls them.
4
+
5
+ ## Adding a new SAST rule
6
+
7
+ 1. **Pick the right module.** If your rule is language-specific (Python sink-side, Kotlin force-unwrap), it goes in or next to the existing language module. If it's framework-specific (Express auth, FastAPI hardening), use a `*-hardening.js`. If it's a cross-cutting class (CSRF, prototype pollution, mass assignment), add a new top-level `<topic>.js`.
8
+ 2. **Export `scan*()`** returning `Finding[]`. Required finding fields: `id` (stable), `severity`, `file`, `line`, `vuln`, `cwe`, `description`, `remediation`. Set `family` and `parser` if you can — defaults in `../posture/finding-defaults.js` will backfill, but a detector-set value is more accurate.
9
+ 3. **Import and call** in `../engine.js`. Find the existing `import { scanX } from './sast/x.js'` block and add yours alphabetically. Call the function inside `runFullScan` so its results flow into `finalFindings`.
10
+ 4. **Fixture pair.** Under `../../test/fixtures/<rule-name>/` create `vulnerable/` and `clean/`. The vulnerable shape must trigger the rule; the clean shape must not.
11
+ 5. **Cover it in tests.** Add to or create a `../../test/<rule-name>.test.js`. Wire it into `npm run test:sast` (or whichever scope it fits) in `../../package.json`.
12
+ 6. **Build + smoke.** `npm run build` then `npm run smoke`. The lifecycle guard (`npm run test:lifecycle`) catches a rule that ships without an import in `engine.js`.
13
+
14
+ ## What lives here, by category
15
+
16
+ **Language-specific** — `cpp.js`, `csharp.js`, `dart-flutter.js`, `go-extended.js`, `java-deserialization.js`, `kotlin.js`, `php.js`, `python-sinks.js`, `ruby.js`, `rust.js`, `solidity.js`, `swift.js`, `xxe.js`.
17
+
18
+ **Framework hardening** — `django-hardening.js`, `fastapi-hardening.js`, `laravel-hardening.js`, `quarkus-hardening.js`, `springboot-hardening.js`. Each detects "you used framework X but forgot the security-hardening step that ships with it" rather than a primary vuln.
19
+
20
+ **Cross-cutting vuln classes** — `authz.js`, `csrf.js`, `host-header.js`, `jndi.js`, `jwt-exp.js`, `ldap-injection.js`, `xpath-injection.js`, `mass-assignment.js`, `mutation-xss.js`, `nosql-injection.js`, `prototype-pollution.js`, `ssrf-cloud-metadata.js`, `toctou.js`, `zip-slip.js`.
21
+
22
+ **Cloud/infra** — `db-rls.js` (Supabase RLS), `env-hygiene.js` (NEXT_PUBLIC_ leaks, .env.example real values), `mobile-manifest.js`, `pipeline.js` (CI/CD integrity), `rate-limit.js`, `webhook.js`.
23
+
24
+ **LLM / agent** — `llm.js`, `llm-owasp.js`, `llm-trading-agent.js`, `mcp-audit.js`, `model-load.js`, `prompt-firewall.js`, `prompt-template.js`.
25
+
26
+ **Agent-of-agent / Claude Code hardening** — `claude-hook-injection.js`, `claude-md-prompt-injection.js`, `claude-settings.js`.
27
+
28
+ **Auth / web** — `auth-provider.js`, `client-side.js`.
29
+
30
+ **Bench-shape adapters (label-leakage, OFF by default)** — `bench-shape/` directory, plus `cpp-bench-extras.js`, `java-bench-extras.js`, `juliet-shape.js`, `primary-cwe-java.js`. These read Juliet / OWASP-Benchmark answer keys. Disabled unless `AGENTIC_SECURITY_BENCH_SHAPE=1`; `AGENTIC_SECURITY_BLIND_BENCH=1` overrides to force everything off. **Never use their emissions as a quality signal.**
31
+
32
+ **Helpers** — `_comment-strip.js` strips comments while preserving line numbers (so finding `line` stays accurate). `index.js` is the public re-export.
33
+
34
+ ## Gotchas
35
+
36
+ - **Comments confuse detectors.** Always go through `blankComments()` from `_comment-strip.js` before scanning a file body.
37
+ - **Detector snippet attribution.** Don't grab `lines[matchLine - 1]` blindly — for multi-line patterns (`exec('ping' + req.body.host, …)`), the match index and the readable sink line can diverge. Premortem item — fixed in the dataflow engine; if you're regressing on it, your fix probably needs to use the actual sink expression, not the regex's match offset.
38
+ - **Severity floor.** No detector should emit `severity: 'critical'` without strong evidence; the calibrator amplifies critical-rated findings and a flood drowns real ones. If in doubt, emit `high` and let `annotateExploitability` push it up.
39
+ - **Family field for calibration.** If you can't tell what family your rule belongs to, that's a signal the rule covers too much. Split it.
@@ -0,0 +1,46 @@
1
+ // Replace comments with same-length whitespace (newlines preserved) so that
2
+ // character indices in the returned string match the original source one-to-one.
3
+ // Required by detectors that emit `line = lineOf(raw, m.index)` after running
4
+ // regexes against a comment-stripped view.
5
+ //
6
+ // Recognises:
7
+ // - JS/TS/Java/Go/C/C++/Rust line comments // ...
8
+ // - JS/TS/Java/Go/C/C++/Rust block comments /* ... */
9
+ // - Python line comments # ...
10
+ //
11
+ // Skips comment-like content inside string literals (single/double/backtick).
12
+ //
13
+ // The `lang` parameter is optional; pass 'py' to treat `#` as a line comment.
14
+
15
+ export function blankComments(s, lang) {
16
+ let out = '';
17
+ let inS = null;
18
+ let i = 0;
19
+ const isPy = lang === 'py';
20
+ while (i < s.length) {
21
+ const c = s[i];
22
+ if (inS) {
23
+ out += c;
24
+ if (c === '\\' && i + 1 < s.length) { out += s[i+1]; i += 2; continue; }
25
+ if (c === inS) inS = null;
26
+ i++; continue;
27
+ }
28
+ if (c === "'" || c === '"' || c === '`') { inS = c; out += c; i++; continue; }
29
+ if (!isPy && c === '/' && s[i+1] === '/') {
30
+ while (i < s.length && s[i] !== '\n') { out += ' '; i++; }
31
+ continue;
32
+ }
33
+ if (!isPy && c === '/' && s[i+1] === '*') {
34
+ const end = s.indexOf('*/', i + 2);
35
+ const stop = end < 0 ? s.length : end + 2;
36
+ while (i < stop) { out += (s[i] === '\n' ? '\n' : ' '); i++; }
37
+ continue;
38
+ }
39
+ if (isPy && c === '#') {
40
+ while (i < s.length && s[i] !== '\n') { out += ' '; i++; }
41
+ continue;
42
+ }
43
+ out += c; i++;
44
+ }
45
+ return out;
46
+ }
@@ -0,0 +1,131 @@
1
+ // Agent Tool-Chain Privilege Escalation (OWASP LLM06 — Sensitive
2
+ // Information Disclosure × LLM07 — Insecure Plugin Design, applied to
3
+ // agent tool composition).
4
+ //
5
+ // Pattern: An agent has two tools — a low-privilege READ tool (`list_files`,
6
+ // `fetch_url`, `query_db_readonly`) and a high-privilege ACT tool (`exec`,
7
+ // `write_file`, `send_email`, `db_admin_query`). The agent's harness lets
8
+ // the LLM call them in sequence: the LLM reads the output of the READ tool
9
+ // (which can be attacker-controlled — files in a tenant's bucket, content
10
+ // from a scraped URL, a row in a DB the attacker can update) and uses
11
+ // that output as input to the ACT tool. The READ tool's output is now an
12
+ // authority-promoting channel.
13
+ //
14
+ // We catch the AT-RISK PATTERN at code-shape time:
15
+ // - Two tools registered in the same agent harness
16
+ // - One is READ-class (callee name matches: read, list, get, fetch,
17
+ // search, query, find, scrape, retrieve)
18
+ // - The other is ACT-class (write, exec, run, send, post, delete,
19
+ // create, update, drop, kill)
20
+ // - No explicit `confirm` / `human_approval` / `policy_check` between
21
+ // them (search for these inside the tool's handler body)
22
+ //
23
+ // Frameworks we recognize:
24
+ // - LangChain Python: Tool(name="…", func=…) / @tool decorator
25
+ // - LangChain JS: new Tool({ name, func })
26
+ // - LangGraph: tools=[…]
27
+ // - OpenAI Assistants: tools=[{ type:"function", function:{ name, …}}]
28
+ // - Anthropic Claude SDK: tools=[{ name, description, input_schema }]
29
+ // - MCP servers: tools.register or capabilities.tools
30
+
31
+ import { blankComments } from './_comment-strip.js';
32
+
33
+ const READ_VERBS = ['read', 'list', 'get', 'fetch', 'search', 'query', 'find', 'scrape', 'retrieve', 'lookup', 'show', 'select'];
34
+ const ACT_VERBS = ['write', 'exec', 'run', 'send', 'post', 'delete', 'create', 'update', 'drop', 'kill', 'execute', 'invoke', 'modify', 'patch', 'mutate', 'add_user', 'remove_user'];
35
+
36
+ // Patterns capturing a tool NAME from a registration line, language-tagged.
37
+ const TOOL_NAME_PATTERNS = [
38
+ // LangChain Python: Tool(name="…", …) or @tool def name(args):
39
+ ['py', /\bTool\s*\(\s*name\s*=\s*['"]([a-zA-Z_][\w]*)['"]/g],
40
+ ['py', /^\s*@tool\s*[\r\n]+\s*def\s+([a-zA-Z_][\w]*)\s*\(/gm],
41
+ // LangChain JS / LangGraph: new Tool({ name: "…" }) or tool({ name })
42
+ ['js', /\bnew\s+Tool\s*\(\s*\{\s*name\s*:\s*['"]([a-zA-Z_][\w]*)['"]/g],
43
+ ['js', /\btool\s*\(\s*\{\s*name\s*:\s*['"]([a-zA-Z_][\w]*)['"]/g],
44
+ // OpenAI Assistants: { type: "function", function: { name: "…" } }
45
+ ['js', /\btype\s*:\s*['"]function['"]\s*,\s*function\s*:\s*\{\s*name\s*:\s*['"]([a-zA-Z_][\w]*)['"]/g],
46
+ ['py', /['"]type['"]\s*:\s*['"]function['"]\s*,\s*['"]function['"]\s*:\s*\{\s*['"]name['"]\s*:\s*['"]([a-zA-Z_][\w]*)['"]/g],
47
+ // Anthropic tools list: { name: "…", description, input_schema }
48
+ ['js', /\{\s*name\s*:\s*['"]([a-zA-Z_][\w]*)['"]\s*,\s*description/g],
49
+ ['py', /\{\s*['"]name['"]\s*:\s*['"]([a-zA-Z_][\w]*)['"]\s*,\s*['"]description['"]/g],
50
+ // MCP server registerTool / setRequestHandler('tools/call', …)
51
+ ['js', /\bregisterTool\s*\(\s*['"]([a-zA-Z_][\w]*)['"]/g],
52
+ ['py', /\b@server\s*\.\s*tool\s*\(\s*['"]([a-zA-Z_][\w]*)['"]/g],
53
+ ];
54
+
55
+ const APPROVAL_HINT_RE =
56
+ /\b(?:requireConfirmation|require_confirmation|human_approval|policy_check|approveAction|approve_action|guardCheck|enforce_policy|capability_check)\b/;
57
+
58
+ function _classify(name) {
59
+ const low = name.toLowerCase();
60
+ for (const v of ACT_VERBS) {
61
+ if (low.includes(v)) return 'act';
62
+ }
63
+ for (const v of READ_VERBS) {
64
+ if (low.includes(v)) return 'read';
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
70
+ function _lang(fp) {
71
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return 'js';
72
+ if (/\.py$/i.test(fp)) return 'py';
73
+ return null;
74
+ }
75
+
76
+ export function scanAgentToolEscalation(fp, raw) {
77
+ if (!raw || raw.length > 500_000) return [];
78
+ const lang = _lang(fp);
79
+ if (!lang) return [];
80
+ const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
81
+ if (!/\b(?:Tool|tool|tools|registerTool|@tool|@server\.tool|input_schema|function_call|tool_use)\b/.test(code)) return [];
82
+ // Collect all tool names declared in this file with their classification.
83
+ const tools = []; // { name, kind: 'act'|'read', line }
84
+ for (const [plang, pat] of TOOL_NAME_PATTERNS) {
85
+ if (plang !== lang) continue;
86
+ const re = new RegExp(pat.source, pat.flags);
87
+ let m;
88
+ while ((m = re.exec(code))) {
89
+ const name = m[1];
90
+ const kind = _classify(name);
91
+ if (!kind) continue;
92
+ tools.push({ name, kind, line: _lineOf(raw, m.index) });
93
+ }
94
+ }
95
+ if (tools.length < 2) return [];
96
+ const hasRead = tools.some(t => t.kind === 'read');
97
+ const hasAct = tools.some(t => t.kind === 'act');
98
+ if (!hasRead || !hasAct) return [];
99
+ // Approval-mechanism gate — if the file references a known confirm/policy
100
+ // helper, we assume the harness mediates the escalation and suppress.
101
+ if (APPROVAL_HINT_RE.test(code)) return [];
102
+ // Fire one finding per ACT tool, naming the READ counterpart in the trace.
103
+ const findings = [];
104
+ const seen = new Set();
105
+ const readNames = tools.filter(t => t.kind === 'read').map(t => t.name);
106
+ for (const actTool of tools.filter(t => t.kind === 'act')) {
107
+ const id = `agent-tool-escalation:${fp}:${actTool.line}:${actTool.name}`;
108
+ if (seen.has(id)) continue;
109
+ seen.add(id);
110
+ findings.push({
111
+ id,
112
+ file: fp, line: actTool.line,
113
+ vuln: `Agent Tool Privilege Escalation (act-tool "${actTool.name}" exposed alongside read-tools)`,
114
+ severity: 'high',
115
+ cwe: 'CWE-269', // Improper Privilege Management
116
+ family: 'agent-tool-escalation',
117
+ stride: 'Elevation of Privilege',
118
+ snippet: (raw.split('\n')[actTool.line - 1] || '').trim().slice(0, 200),
119
+ remediation:
120
+ `Tool "${actTool.name}" performs an action (write/exec/send/delete). It's registered alongside read tools (${readNames.slice(0, 4).join(', ')}) in the same agent surface — the LLM can pipe the read tool's output (which is attacker-influenceable: scraped pages, DB rows tenants can edit, file contents in shared buckets) directly into the act tool's input. ` +
121
+ 'Mitigations: ' +
122
+ '(1) require an explicit confirmation / human approval step for any act tool whose inputs are derived from a read tool\'s output; ' +
123
+ '(2) isolate act tools to a separate agent surface with a stricter system prompt; ' +
124
+ '(3) attach a policy_check / capability_check helper to the act tool that verifies the input was not solely derived from low-trust read sources; ' +
125
+ '(4) tag read-tool outputs with provenance metadata so the act tool can refuse low-trust input.',
126
+ parser: 'AGENT-TOOL-ESCALATION',
127
+ confidence: 0.7,
128
+ });
129
+ }
130
+ return findings;
131
+ }
@@ -0,0 +1,171 @@
1
+ // Auth provider misconfiguration audit.
2
+ //
3
+ // Every popular auth library for vibecoders has provider-specific footguns.
4
+ // This module detects the most common misconfigurations that static analysis
5
+ // can catch: insecure options, missing required fields, dangerous flags.
6
+ //
7
+ // Providers covered:
8
+ // Clerk, NextAuth / Auth.js, Auth0, Lucia, Better Auth, Passport, generic
9
+ //
10
+ // Findings:
11
+ // AUTH_DANGEROUS_EMAIL_LINKING — allowDangerousEmailAccountLinking: true
12
+ // AUTH_TRUST_HOST — trustHost: true in NextAuth (CSRF bypass in prod)
13
+ // AUTH_MISSING_SECRET — NextAuth without NEXTAUTH_SECRET env var reference
14
+ // AUTH_WEAK_SESSION_SECRET — short/hardcoded session secret
15
+ // AUTH_CLERK_PUBLIC_ROUTE — sensitive path incorrectly marked public in Clerk
16
+ // AUTH_MISSING_AUDIENCE — JWT/OAuth without audience validation
17
+ // AUTH_DISABLE_CSRF — explicit CSRF protection disabled
18
+ // AUTH_COOKIE_INSECURE — session cookies without secure/sameSite
19
+ // AUTH_HARDCODED_CLIENT_SECRET — OAuth clientSecret hardcoded
20
+
21
+ const _SCAN_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i;
22
+ const _NONPROD_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|node_modules)\//i;
23
+
24
+ // --- Dangerous options ---
25
+
26
+ // allowDangerousEmailAccountLinking: true — lets attacker take over account by
27
+ // registering with same email via different provider
28
+ const DANGEROUS_EMAIL_LINKING_RE = /allowDangerousEmailAccountLinking\s*:\s*true/;
29
+
30
+ // trustHost: true in NextAuth — disables host validation, enabling CSRF when
31
+ // deployed behind a reverse proxy that sets arbitrary Host headers
32
+ const TRUST_HOST_RE = /trustHost\s*:\s*true/;
33
+
34
+ // NextAuth without NEXTAUTH_SECRET
35
+ const NEXTAUTH_IMPORT_RE = /(?:from|require)\s*\(?\s*['"`]next-auth['"`]/;
36
+ const NEXTAUTH_SECRET_REF_RE = /NEXTAUTH_SECRET|authSecret\s*:|secret\s*:\s*process\.env/;
37
+
38
+ // Weak session secret — short string literal as secret
39
+ const WEAK_SESSION_SECRET_RE = /(?:secret|SESSION_SECRET|sessionSecret)\s*[:=]\s*['"`]([^'"`]{1,20})['"`]/;
40
+
41
+ // CSRF disabled
42
+ const CSRF_DISABLED_RE = /csrf\s*:\s*(?:false|disabled|0)|disableCsrf\s*:\s*true|csrfProtection\s*:\s*false/i;
43
+
44
+ // OAuth clientSecret hardcoded
45
+ const HARDCODED_CLIENT_SECRET_RE = /clientSecret\s*:\s*['"`][a-zA-Z0-9_\-]{8,}['"`]/;
46
+
47
+ // JWT missing audience
48
+ const JWT_NO_AUDIENCE_RE = /jwt\.(?:verify|sign)\s*\([^)]*\)/g;
49
+ const JWT_AUDIENCE_RE = /\baudience\b/;
50
+
51
+ // Auth0 hardcoded secret
52
+ const AUTH0_SECRET_RE = /AUTH0_SECRET\s*[:=]\s*['"`][^'"`]{8,}['"`]/;
53
+
54
+ // Cookie without secure flag
55
+ const INSECURE_COOKIE_RE = /cookie\s*:\s*\{[^}]*\}/g;
56
+ const COOKIE_SECURE_RE = /\bsecure\s*:\s*true/;
57
+ const COOKIE_SAMESITE_RE = /\bsameSite\s*:/;
58
+
59
+ // Clerk: publicRoutes containing sensitive paths
60
+ const CLERK_CONFIG_RE = /(?:clerkMiddleware|authMiddleware)\s*\(\s*\{/;
61
+ const CLERK_PUBLIC_ROUTES_RE = /publicRoutes\s*:\s*\[([^\]]+)\]/;
62
+ const SENSITIVE_PATH_IN_PUBLIC_RE = /['"`]\/(?:api\/(?:admin|users|delete|update|private)|dashboard|settings|admin)[^'"`]*['"`]/i;
63
+
64
+ function scanAuthProvider(file, content) {
65
+ if (!_SCAN_EXT_RE.test(file)) return [];
66
+ if (_NONPROD_RE.test(file)) return [];
67
+ const findings = [];
68
+ const lines = content.split('\n');
69
+
70
+ function lineOf(re, searchContent = content) {
71
+ const m = re.exec(searchContent);
72
+ if (!m) return -1;
73
+ return searchContent.slice(0, m.index).split('\n').length;
74
+ }
75
+
76
+ function push(id, title, severity, lineNum, description, remediation, cwe) {
77
+ if (lineNum < 1) lineNum = 1;
78
+ findings.push({ id: `auth-provider:${id}:${file}:${lineNum}`, title, severity, file, line: lineNum, description, remediation, cwe });
79
+ }
80
+
81
+ // allowDangerousEmailAccountLinking
82
+ if (DANGEROUS_EMAIL_LINKING_RE.test(content)) {
83
+ push('AUTH_DANGEROUS_EMAIL_LINKING', 'Dangerous email account linking enabled',
84
+ 'high', lineOf(DANGEROUS_EMAIL_LINKING_RE),
85
+ 'allowDangerousEmailAccountLinking: true lets an attacker register with the same email address via a different OAuth provider to take over an existing account without knowing the password.',
86
+ 'Remove allowDangerousEmailAccountLinking: true. If you need multi-provider linking, implement explicit user-confirmation flow instead.',
87
+ 'CWE-287');
88
+ }
89
+
90
+ // trustHost
91
+ if (TRUST_HOST_RE.test(content)) {
92
+ push('AUTH_TRUST_HOST', 'NextAuth trustHost: true disables host validation',
93
+ 'high', lineOf(TRUST_HOST_RE),
94
+ 'trustHost: true bypasses NextAuth\'s HOST header validation, disabling CSRF protection when deployed behind a reverse proxy. Attackers can craft requests with a spoofed Host header.',
95
+ 'Remove trustHost: true. Instead, set the AUTH_URL / NEXTAUTH_URL environment variable to your canonical production URL.',
96
+ 'CWE-352');
97
+ }
98
+
99
+ // NextAuth without secret
100
+ if (NEXTAUTH_IMPORT_RE.test(content) && !NEXTAUTH_SECRET_REF_RE.test(content)) {
101
+ push('AUTH_MISSING_SECRET', 'NextAuth configuration without NEXTAUTH_SECRET reference',
102
+ 'high', lineOf(NEXTAUTH_IMPORT_RE),
103
+ 'NextAuth requires NEXTAUTH_SECRET for JWT encryption and CSRF token signing. Without it, NextAuth falls back to an auto-generated secret that changes on every restart, invalidating all sessions, and is insecure in some deploy environments.',
104
+ 'Add `secret: process.env.NEXTAUTH_SECRET` to your NextAuth config and set NEXTAUTH_SECRET to a 32+ byte random string in your environment.',
105
+ 'CWE-330');
106
+ }
107
+
108
+ // Weak session secret
109
+ for (let i = 0; i < lines.length; i++) {
110
+ const m = WEAK_SESSION_SECRET_RE.exec(lines[i]);
111
+ if (m && m[1] && m[1].length <= 20) {
112
+ push('AUTH_WEAK_SESSION_SECRET', 'Session secret is short or hardcoded',
113
+ 'high', i + 1,
114
+ `A session/auth secret of only ${m[1].length} characters is used. Secrets shorter than 32 characters are vulnerable to brute-force. Hardcoded secrets are also committed to git history.`,
115
+ 'Generate a cryptographically random 32+ byte secret: `openssl rand -base64 32`. Store it as an environment variable, never in source code.',
116
+ 'CWE-521');
117
+ }
118
+ }
119
+
120
+ // Hardcoded OAuth clientSecret
121
+ for (let i = 0; i < lines.length; i++) {
122
+ if (HARDCODED_CLIENT_SECRET_RE.test(lines[i])) {
123
+ push('AUTH_HARDCODED_CLIENT_SECRET', 'OAuth clientSecret hardcoded in source',
124
+ 'high', i + 1,
125
+ 'An OAuth client secret is embedded in source code. It will be committed to git, potentially leaked via the build bundle, and cannot be rotated without a code change.',
126
+ 'Move to an environment variable: `clientSecret: process.env.OAUTH_CLIENT_SECRET`. Rotate the secret in your OAuth provider dashboard.',
127
+ 'CWE-798');
128
+ }
129
+ }
130
+
131
+ // CSRF disabled
132
+ if (CSRF_DISABLED_RE.test(content)) {
133
+ push('AUTH_DISABLE_CSRF', 'CSRF protection explicitly disabled',
134
+ 'high', lineOf(CSRF_DISABLED_RE),
135
+ 'Cross-Site Request Forgery protection is turned off. Attackers can trick authenticated users into triggering state-changing actions on your app from a third-party website.',
136
+ 'Remove the csrf: false / disableCsrf option. Auth libraries enable CSRF protection by default for good reason.',
137
+ 'CWE-352');
138
+ }
139
+
140
+ // Clerk public routes containing sensitive paths
141
+ if (CLERK_CONFIG_RE.test(content)) {
142
+ const publicMatch = CLERK_PUBLIC_ROUTES_RE.exec(content);
143
+ if (publicMatch && SENSITIVE_PATH_IN_PUBLIC_RE.test(publicMatch[1])) {
144
+ const lineNum = content.slice(0, CLERK_PUBLIC_ROUTES_RE.lastIndex).split('\n').length;
145
+ push('AUTH_CLERK_PUBLIC_ROUTE', 'Sensitive route marked public in Clerk middleware',
146
+ 'high', lineNum,
147
+ 'A path that appears to be sensitive (admin, settings, private API) is listed in Clerk\'s publicRoutes, making it accessible to unauthenticated users.',
148
+ 'Remove sensitive paths from publicRoutes. Use auth().protect() or redirect to sign-in for routes that require authentication.',
149
+ 'CWE-284');
150
+ }
151
+ }
152
+
153
+ // Cookie without secure/sameSite in session config
154
+ let cookieM;
155
+ const cookieRe = new RegExp(INSECURE_COOKIE_RE.source, 'g');
156
+ while ((cookieM = cookieRe.exec(content)) !== null) {
157
+ const block = cookieM[0];
158
+ if (!COOKIE_SECURE_RE.test(block) || !COOKIE_SAMESITE_RE.test(block)) {
159
+ const lineNum = content.slice(0, cookieM.index).split('\n').length;
160
+ push('AUTH_COOKIE_INSECURE', 'Session cookie missing secure or sameSite flag',
161
+ 'medium', lineNum,
162
+ 'Session cookies without `secure: true` can be transmitted over HTTP. Without `sameSite`, they are sent on cross-site requests, enabling CSRF.',
163
+ 'Set `cookie: { secure: true, sameSite: "lax", httpOnly: true }` in your session/auth config.',
164
+ 'CWE-614');
165
+ }
166
+ }
167
+
168
+ return findings;
169
+ }
170
+
171
+ export { scanAuthProvider };
@@ -0,0 +1,236 @@
1
+ // Authentication / authorization deep-analysis detector.
2
+ //
3
+ // OWASP A01: Broken Access Control is consistently the #1 source of real
4
+ // breaches. The taint-based pipeline already catches IDOR-by-id and SQLi via
5
+ // auth-table lookups. This module covers the higher-level patterns that pure
6
+ // data-flow misses:
7
+ //
8
+ // - JWT alg:none / algorithm confusion
9
+ // - Hardcoded JWT secret
10
+ // - jwt.verify called without an algorithms allow-list
11
+ // - OAuth2 authorization_code flow with no PKCE
12
+ // - OAuth2 redirect_uri taken from the request without allowlist validation
13
+ // - Session not regenerated after authentication (session fixation)
14
+ // - Multi-tenant query missing a tenant scope (cross-tenant read)
15
+ //
16
+ // F1 strategy:
17
+ // Each pattern fires only when there is concrete signal in source. Negative
18
+ // contexts (allow-list present, tenant filter present, PKCE generation
19
+ // present in the same module) suppress the finding.
20
+
21
+ const _SCAN_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py)$/i;
22
+ const _NONPROD_PATH_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|docs?|stories|codefixes|node_modules)\//i;
23
+
24
+ // --- JWT patterns ---
25
+
26
+ // jwt.sign or jwt.verify with explicit alg: 'none' / "none"
27
+ const JWT_ALG_NONE_RE = /\b(?:jwt|jsonwebtoken)\.(?:sign|verify|decode)\s*\([^)]*?(?:algorithm|alg)\s*:\s*['"]none['"]/i;
28
+
29
+ // JWT_SECRET / signingKey hardcoded as a literal short string in source
30
+ const JWT_HARDCODED_SECRET_RE = /\b(?:JWT_SECRET|jwtSecret|jwt_secret|signingSecret|signing_key|JWT_KEY)\s*[:=]\s*['"]([^'"]{4,64})['"]/;
31
+
32
+ // jwt.verify called without an `algorithms` option (algorithm confusion attack)
33
+ // We require: a `jwt.verify(` call within ~200 chars of a missing `algorithms`
34
+ const JWT_VERIFY_RE = /\b(?:jwt|jsonwebtoken)\.verify\s*\(/g;
35
+ const JWT_ALGORITHMS_OPT_RE = /\balgorithms\s*:\s*\[/;
36
+
37
+ // --- OAuth2 / OIDC ---
38
+
39
+ // authorization_code flow without PKCE: matches `response_type: 'code'` with no
40
+ // `code_challenge` in surrounding context.
41
+ const OAUTH_AUTHCODE_RE = /\bresponse_type\s*[:=]\s*['"]code['"]/;
42
+ const OAUTH_PKCE_RE = /\b(?:code_challenge|codeChallenge|pkce|code_verifier)\b/;
43
+
44
+ // redirect_uri taken from req without allow-list. We trigger when:
45
+ // redirectUrl/url = req.query.redirect_uri (or .body / .params)
46
+ // and the same module has no constants[*] === url style allow-list.
47
+ const OAUTH_REDIRECT_FROM_REQ_RE = /\b(?:redirect|redirectUri|redirect_uri|callback|returnTo|returnUrl|next)\s*[:=]\s*(?:req|request)\.(?:query|body|params)\.[A-Za-z_]\w*/i;
48
+ const OAUTH_REDIRECT_ALLOWLIST_RE = /\b(?:ALLOWED_REDIRECTS|REDIRECT_ALLOWLIST|allowedRedirects|allowedHosts|isAllowed(?:Url|Host|Redirect)|VALID_REDIRECTS)\b|\.includes\s*\(\s*(?:redirect|redirectUri|redirect_uri|returnTo|callback|next|url)\s*\)/;
49
+
50
+ // --- Session fixation ---
51
+
52
+ // Pattern: an authentication step (req.login, passport.authenticate completion,
53
+ // `req.session.userId = ...`) followed by no `session.regenerate(` call.
54
+ const SESSION_LOGIN_RE = /\b(?:req\.login\s*\(|req\.session\.(?:userId|user_id|user|uid)\s*=|passport\.authenticate\s*\([^)]*\)\s*\(req|request\.session\['user'\]\s*=)/;
55
+ const SESSION_REGENERATE_RE = /\b(?:req\.session\.regenerate\s*\(|session\.regenerate\s*\(|request\.session\.cycle_key\s*\(|sessionStore\.regenerate)/;
56
+
57
+ // --- Multi-tenant scope ---
58
+
59
+ // SELECT/find with a where-clause keyed by a non-tenant id (Sequelize, Prisma,
60
+ // raw SQL, mongoose). Suppress if the same statement contains tenantId/orgId/
61
+ // workspaceId in the where clause.
62
+ const MT_QUERY_RE = /\b(?:findOne|findById|findFirst|findUnique|find\(\s*\{)\s*[^;]*?\bwhere\s*:\s*\{[^}]*\bid\s*:\s*(?:req|request)\.(?:params|body|query)\.[A-Za-z_]\w*[^}]*\}/i;
63
+ const MT_TENANT_KEY_RE = /\b(?:tenantId|tenant_id|orgId|org_id|workspaceId|workspace_id|accountId|account_id|companyId|company_id)\b/;
64
+
65
+ // Raw SQL with direct interpolation of a request value into the WHERE-by-id
66
+ // clause. We deliberately do NOT match parameterized placeholders (?, $1, :id)
67
+ // — those are the safe pattern. Only flag string-concatenation or template-
68
+ // literal interpolation that pulls from req/request.
69
+ const MT_RAW_SQL_RE = /\b(?:select|update|delete)\b[\s\S]{0,200}?\bwhere\s+(?:[\w_.]*\.)?id\s*=\s*\$?\{?\s*(?:req|request)\.(?:params|body|query)\.[A-Za-z_]\w*/i;
70
+
71
+ function _emit(fp, line, vuln, severity, cwe, snippet, fix, confidence=0.85) {
72
+ return {
73
+ id: `authz:${fp}:${line}:${vuln.replace(/[^A-Za-z0-9]/g, '_').slice(0, 60)}`,
74
+ kind: 'authz', severity, vuln,
75
+ cwe, stride: 'Elevation of Privilege',
76
+ file: fp, line, snippet: (snippet || '').trim().slice(0, 200),
77
+ fix, confidence,
78
+ };
79
+ }
80
+
81
+ // Strip string-literal contents while preserving line/col so the raw-SQL and
82
+ // shape-only patterns below don't self-detect inside fix-message templates or
83
+ // other string-embedded examples.
84
+ function _stripStrings(code){
85
+ const out = code.split('');
86
+ const n = code.length;
87
+ let i = 0, state = 0; // 0 NORMAL, 1 SQ, 2 DQ, 3 BT
88
+ while (i < n) {
89
+ const c = code[i];
90
+ if (state === 0) {
91
+ if (c === "'") { state = 1; i++; continue; }
92
+ if (c === '"') { state = 2; i++; continue; }
93
+ if (c === '`') { state = 3; i++; continue; }
94
+ i++; continue;
95
+ }
96
+ const quote = state === 1 ? "'" : state === 2 ? '"' : '`';
97
+ if (c === '\\' && i + 1 < n) { if (code[i+1] !== '\n') out[i+1] = ' '; out[i] = ' '; i += 2; continue; }
98
+ if (c === quote) { state = 0; i++; continue; }
99
+ if (state === 3 && c === '$' && code[i+1] === '{') {
100
+ // Preserve template expression content: skip ahead until matching }.
101
+ let depth = 1; out[i]='$'; out[i+1]='{'; i += 2;
102
+ while (i < n && depth > 0) {
103
+ if (code[i] === '{') depth++;
104
+ else if (code[i] === '}') depth--;
105
+ i++;
106
+ }
107
+ continue;
108
+ }
109
+ if (c !== '\n') out[i] = ' ';
110
+ i++;
111
+ }
112
+ return out.join('');
113
+ }
114
+
115
+ export function scanAuthZ(fp, raw) {
116
+ if (!_SCAN_EXT_RE.test(fp)) return [];
117
+ const fpNorm = fp.replace(/\\/g, '/');
118
+ if (_NONPROD_PATH_RE.test(fpNorm)) return [];
119
+ if (!raw || raw.length > 500_000) return [];
120
+
121
+ // `rawForShape` is used by detectors that match on code shape (raw SQL, JWT
122
+ // calls). String literals are blanked so fix-message templates and example
123
+ // snippets don't self-detect. Detectors that explicitly read literal content
124
+ // (hardcoded JWT secret) keep using `raw`.
125
+ const rawForShape = _stripStrings(raw);
126
+ const linesShape = rawForShape.split('\n');
127
+ const lines = raw.split('\n');
128
+ const findings = [];
129
+ const seen = new Set();
130
+ const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
131
+
132
+ // Per-line patterns
133
+ for (let i = 0; i < lines.length; i++) {
134
+ const ln = lines[i];
135
+
136
+ // 1. JWT alg:none
137
+ if (JWT_ALG_NONE_RE.test(ln)) {
138
+ push(_emit(fp, i + 1,
139
+ 'AuthZ: JWT alg:none accepted (forgery)',
140
+ 'critical', 'CWE-347', ln,
141
+ 'Setting algorithm to "none" disables signature verification — any token is accepted as valid. Set an explicit `algorithms: ["RS256"]` (or HS256 for symmetric) and reject tokens that present a different alg.'));
142
+ }
143
+
144
+ // 2. Hardcoded JWT secret (shortish literal)
145
+ const m2 = ln.match(JWT_HARDCODED_SECRET_RE);
146
+ if (m2) {
147
+ const val = m2[1];
148
+ // Suppress only template/env placeholders
149
+ if (!/process\.env|\$\{|<.*?>|^\s*$/.test(val) && !/\bsecret\b|\bchange.?me\b|^example$/i.test(val) === false || val.length >= 4) {
150
+ // We still flag well-known placeholders ("secret", "changeme") because they
151
+ // are the most common production foot-gun.
152
+ push(_emit(fp, i + 1,
153
+ 'AuthZ: hardcoded JWT secret in source',
154
+ 'critical', 'CWE-798', ln.replace(val, '<redacted>'),
155
+ 'Move the JWT secret to an environment variable or secret store, and rotate the previous value (it must be considered leaked). For asymmetric tokens, prefer RS256 with the private key in a KMS.'));
156
+ }
157
+ }
158
+
159
+ // 3. authorization_code flow without PKCE — needs whole-file context.
160
+ if (OAUTH_AUTHCODE_RE.test(ln)) {
161
+ const hasPkceNearby = OAUTH_PKCE_RE.test(raw);
162
+ if (!hasPkceNearby) {
163
+ push(_emit(fp, i + 1,
164
+ 'AuthZ: OAuth2 authorization_code without PKCE',
165
+ 'high', 'CWE-287', ln,
166
+ 'Public OAuth2 clients (SPAs, mobile, native) must use PKCE. Generate a `code_verifier` (43–128 chars), derive a `code_challenge = base64url(sha256(verifier))`, send it on the authorize call, and verify it on the token exchange.'));
167
+ }
168
+ }
169
+
170
+ // 4. redirect_uri taken from request — flag if no allow-list anywhere in file
171
+ if (OAUTH_REDIRECT_FROM_REQ_RE.test(ln)) {
172
+ const hasAllowlist = OAUTH_REDIRECT_ALLOWLIST_RE.test(raw);
173
+ if (!hasAllowlist) {
174
+ push(_emit(fp, i + 1,
175
+ 'AuthZ: OAuth2 redirect_uri from request without allow-list',
176
+ 'high', 'CWE-601', ln,
177
+ 'Validate the redirect_uri against a server-side allow-list before redirecting. An attacker can register a malicious client or pass `?redirect=evil.com` and intercept the authorization code or open-redirect to a phishing page.'));
178
+ }
179
+ }
180
+ }
181
+
182
+ // 5. jwt.verify without algorithms option
183
+ let vm;
184
+ const verifyRe = new RegExp(JWT_VERIFY_RE.source, 'g');
185
+ while ((vm = verifyRe.exec(raw))) {
186
+ // window is the call-site argument list
187
+ const after = raw.slice(vm.index, Math.min(raw.length, vm.index + 400));
188
+ if (!JWT_ALGORITHMS_OPT_RE.test(after) && !JWT_ALG_NONE_RE.test(after)) {
189
+ const line = raw.substring(0, vm.index).split('\n').length;
190
+ push(_emit(fp, line,
191
+ 'AuthZ: jwt.verify called without algorithms allow-list',
192
+ 'high', 'CWE-347', lines[line - 1] || '',
193
+ 'Pass `{ algorithms: ["RS256"] }` (or HS256) explicitly to `jwt.verify`. Without it, an attacker can forge a token using an unexpected algorithm (alg:none, or HS256-signed with the public key for an RS256 issuer).'));
194
+ }
195
+ }
196
+
197
+ // 6. Session fixation: login without regenerate
198
+ if (SESSION_LOGIN_RE.test(raw) && !SESSION_REGENERATE_RE.test(raw)) {
199
+ const m = raw.match(SESSION_LOGIN_RE);
200
+ if (m) {
201
+ const line = raw.substring(0, m.index).split('\n').length;
202
+ push(_emit(fp, line,
203
+ 'AuthZ: session not regenerated after authentication (session fixation)',
204
+ 'high', 'CWE-384', lines[line - 1] || '',
205
+ 'After successful authentication, call `req.session.regenerate(...)` (or your framework equivalent) before storing the user identity in the session. Otherwise an attacker who fixed the pre-auth session id retains access post-login.'));
206
+ }
207
+ }
208
+
209
+ // 7. Multi-tenant: where-by-id without tenant scope
210
+ let mm;
211
+ const mtRe = new RegExp(MT_QUERY_RE.source, 'gi');
212
+ while ((mm = mtRe.exec(raw))) {
213
+ const block = mm[0];
214
+ if (!MT_TENANT_KEY_RE.test(block)) {
215
+ const line = raw.substring(0, mm.index).split('\n').length;
216
+ push(_emit(fp, line,
217
+ 'AuthZ: tenant-scoped query missing tenantId/orgId filter',
218
+ 'high', 'CWE-639', lines[line - 1] || block.slice(0, 120),
219
+ 'Multi-tenant queries must include the requesting user\'s tenantId/orgId in the WHERE clause. Otherwise a row id collision (or guessing) reads another tenant\'s data. Add `where: { id, tenantId: req.user.tenantId }`.'));
220
+ }
221
+ }
222
+ let rm;
223
+ const rawSqlRe = new RegExp(MT_RAW_SQL_RE.source, 'gi');
224
+ while ((rm = rawSqlRe.exec(rawForShape))) {
225
+ const block = rm[0];
226
+ if (!MT_TENANT_KEY_RE.test(block)) {
227
+ const line = rawForShape.substring(0, rm.index).split('\n').length;
228
+ push(_emit(fp, line,
229
+ 'AuthZ: raw SQL where-by-id without tenant scope',
230
+ 'high', 'CWE-639', lines[line - 1] || block.slice(0, 120),
231
+ 'The query selects a row by id without scoping to the caller\'s tenant. Append `AND tenant_id = $tenantId` (and pass it from the authenticated session, never the request body).'));
232
+ }
233
+ }
234
+
235
+ return findings;
236
+ }